File Coverage

blib/lib/SWF/NeedsRecompile.pm
Criterion Covered Total %
statement 223 255 87.4
branch 61 90 67.7
condition 3 6 50.0
subroutine 24 27 88.8
pod 7 7 100.0
total 318 385 82.6


line stmt bran cond sub pod time code
1             package SWF::NeedsRecompile;
2              
3 1     1   22747 use warnings;
  1         2  
  1         35  
4 1     1   6 use strict;
  1         2  
  1         29  
5 1     1   21 use 5.008; # tested only on 5.8.6+, but *should* work on older perls
  1         3  
  1         37  
6 1     1   5 use Carp;
  1         2  
  1         90  
7 1     1   796 use English qw(-no_match_vars);
  1         4115  
  1         5  
8 1     1   393 use File::Spec;
  1         2  
  1         21  
9 1     1   900 use File::Slurp qw();
  1         14509  
  1         29  
10 1     1   1022 use Regexp::Common qw(comment);
  1         5786  
  1         4  
11 1     1   11026 use File::HomeDir;
  1         7296  
  1         80  
12 1     1   963 use List::MoreUtils qw(any);
  1         1238  
  1         100  
13              
14             our $VERSION = '1.06';
15              
16 1     1   6 use base qw(Exporter);
  1         2  
  1         3367  
17             our @EXPORT_OK = qw(
18             check_files
19             as_classpath
20             flash_prefs_path
21             flash_config_path
22             );
23              
24             my $cached_as_classpath;
25              
26             my $verbosity = 0;
27             __PACKAGE__->set_verbosity($ENV{SWFCOMPILE_VERBOSITY});
28              
29             my %os_paths = (
30             darwin => {
31             pref => [File::Spec->catfile(File::HomeDir->my_home, 'Library', 'Preferences', 'Flash 7 Preferences')],
32             conf => [File::Spec->catfile(File::HomeDir->my_home, 'Library', 'Application Support', 'Macromedia',
33             'Flash MX 2004', 'en', 'Configuration')],
34             },
35             # TODO: add more entries for "MSWin32", etc
36             );
37             # These are mostly Flash 6 component classes
38             my %exceptions = map { $_ => 1 } qw(
39             DataProviderClass
40             FScrollSelectListClass
41             FSelectableItemClass
42             FSelectableListClass
43             FStyleFormat
44             FUIComponentClass
45             Tween
46             );
47              
48             sub _get_os_paths # FOR TESTING ONLY!!!
49             {
50 1     1   40 return \%os_paths;
51             }
52              
53             =for stopwords Actionscript Classpath MTASC MX SWF .swf .fla timestamp wildcards UCS2 Dolan Macromedia
54              
55             =head1 NAME
56              
57             SWF::NeedsRecompile - Tests if any SWF or FLA file dependencies have changed
58              
59             =head1 LICENSE
60              
61             Copyright 2002-2006 Clotho Advanced Media, Inc.,
62             L
63              
64             Copyright 2007-2008 Chris Dolan
65              
66             This library is free software; you can redistribute it and/or modify
67             it under the same terms as Perl itself.
68              
69             =head1 SYNOPSIS
70              
71             use SWF::NeedsRecompile qw(check_files);
72             foreach my $file (check_files(<*.swf>)) {
73             print "SWF needs recompilation: $file\n";
74             }
75              
76             =head1 DESCRIPTION
77              
78             This module parses .fla and .as files and determines dependencies
79             recursively, via import and #include statements. It then compares the
80             timestamps of all of the dependencies against the timestamp of the
81             .swf file. If any dependency is newer than the .swf, that file needs
82             to be recompiled.
83              
84             =head1 LIMITATIONS
85              
86             This module only works in its entirety on Mac OS X, and for Flash MX
87             2004. Help wanted: extend it to Windows (add appropriate search paths
88             at the top of the .pm file) and extend it to the Flash 8 author when
89             that is available.
90              
91             This module only reports whether or not the .swf is up to date. It
92             would be useful to know whether it is out of date because of the .fla
93             file or any .as files. In the latter case, the open source MTASC
94             (L) application could perform the
95             recompilation.
96              
97             This module likely only works with ASCII file names. The heuristic
98             used to parse the binary .fla files discards the upper Unicode byte of
99             any file names.
100              
101             If there are C statements with wildcards in any .as files,
102             then all files in the specified directory are considered dependencies,
103             even if only a subset are actually used.
104              
105             Direct access to class methods are not detected. So, if you
106             Actionscript does something like C then
107             com/example/Foo.as is not detected as a dependency. The workaround is
108             to add an import; in this example it would be
109             C
110              
111             =head1 FUNCTIONS
112              
113             =over
114              
115             =item check_files($file, $file, ...)
116              
117             Examine a list of .swf and/or .fla files and return the file names of
118             the ones that need to be recompiled.
119              
120             Performance note: Information is cached across files, so it's faster
121             to call this function once with a bunch of files than a bunch of times
122             with one file each invocation.
123              
124             =cut
125              
126             sub check_files
127             {
128 39     39 1 20570 my @files = @_;
129              
130 39         68 my @needs_recompile;
131              
132             # The depends hash is a cache of the #include and import lines in each file
133             my %depends;
134              
135 39         83 foreach my $file (@files)
136             {
137 39         301 (my $base = $file) =~ s/[.](?:swf|fla)\z//xms;
138 39 100       352 if ($base eq $file)
139             {
140 1         7 _log(1, "$file is not a .swf or a .fla file");
141 1         4 next;
142             }
143 38         90 my $swf = "$base.swf";
144 38         80 my $fla = "$base.fla";
145              
146             # Do the simple case first
147 38 100       596 if (! -e $swf)
148             {
149 3         10 _log(1, "No file $swf");
150 3         4 push @needs_recompile, $file;
151 3         8 next;
152             }
153              
154             # Look for FLA-specific Classpaths
155 35         106 my @paths = _get_fla_classpaths($fla);
156              
157             # Check all SWF dependencies, recursively
158 35         65 my @check = ($fla);
159 35         43 my %checked;
160 35         44 my $up_to_date = 1;
161 35         88 while (@check > 0)
162             {
163 496         680 my $checkfile = pop @check;
164 496 100       1342 next if ($checked{$checkfile});
165              
166 300 100       7733 if (! -f $checkfile)
167             {
168 1         6 _log(1, "Failed to locate file needed to compile $swf: $checkfile");
169 1         3 $up_to_date = 0;
170 1         2 last;
171             }
172              
173 299         937 _log(2, "check $checkfile");
174 299         627 $up_to_date = _up_to_date($checkfile, $swf);
175 299         963 $checked{$checkfile} = 1;
176 299 100       556 if (!$up_to_date)
177             {
178 11         48 _log(1, "Failed up to date check for $checkfile vs. $swf");
179 11         24 last;
180             }
181              
182 288 50       6320 if (! -r $checkfile)
183             {
184 0         0 _log(1, "Unreadable file $checkfile");
185 0         0 last;
186             }
187              
188 288 50       746 if (!$depends{$checkfile})
189             {
190 288         731 _log(2, "do deps for $checkfile");
191 288         1117 $depends{$checkfile} = [];
192 288         778 my $content = File::Slurp::read_file($checkfile);
193 288         29633 my %imported_files;
194             my %seen;
195              
196 288 100       1299 if ($checkfile =~ m/[.]fla\z/ixms)
197             {
198             # HACK: use C regexp because the ECMAScript regexp can
199             # cause an infinite loop on some .fla files.
200             # See BUGS AND LIMITATIONS
201 33         241 $content =~ s/$RE{comment}{C}//gxms;
202             }
203             else
204             {
205 255         1455 $content =~ s/$RE{comment}{ECMAScript}//gxms;
206             }
207              
208             # check for include and import statements and instantiations via "new"
209 288         31546 my @deps = (
210             _get_includes($checkfile, \$content, \%seen),
211             _get_imports($checkfile, \$content, \@paths, \%imported_files, \%seen),
212             _get_instantiations($checkfile, \$content, \@paths, \%imported_files, \%seen),
213             );
214 288         618 my @problems = map { @{$_} } grep { ref $_ } @deps;
  1         2  
  1         4  
  494         911  
215 288 100       686 if (@problems > 0)
216             {
217 1         7 _log(1, "Failed to locate dependencies in $checkfile: @problems");
218 1         2 $up_to_date = 0;
219 1         4 last;
220             }
221 287         1226 $depends{$checkfile} = \@deps;
222             }
223 287         440 push @check, @{$depends{$checkfile}};
  287         1049  
224             }
225              
226 35 100       172 if (!$up_to_date)
227             {
228 13         63 push @needs_recompile, $file;
229             }
230             }
231 39         493 return @needs_recompile;
232             }
233              
234             sub _get_fla_classpaths
235             {
236 35     35   56 my $fla = shift;
237              
238 35         64 my @paths;
239 35 100 66     567 if (-f $fla && (my $content = File::Slurp::read_file($fla, binmode => ':raw')))
240             {
241             # Limitation: the path must be purely ASCII or this doesn't work
242 34         7719 my @matches = $content =~ m/V\0e\0c\0t\0o\0r\0:\0:\0P\0a\0c\0k\0a\0g\0e\0[ ]\0P\0a\0t\0h\0s\0....((?:[^\0]\0)*)/gxms;
243 34         66 my %seen;
244 34         65 for my $match (@matches)
245             {
246             # Hack: downgrade unicode to ascii
247 67         273 $match =~ s/\0//gxms;
248 67 100       198 next if q{} eq $match;
249 64         175 my @search_paths = split m/;/xms, $match;
250 64         416 require File::Spec;
251 64         114 for my $path (@search_paths)
252             {
253 64 50       517 if (!File::Spec->file_name_is_absolute($path))
254             {
255 64         829 my $root = [File::Spec->splitpath($fla)]->[1];
256 64 50       254 if ($root)
257             {
258 64         1877 $path = File::Spec->rel2abs($path, $root);
259             }
260             }
261 64 100       363 next if ($seen{$path}++);
262 32         109 push @paths, $path;
263             }
264             }
265 34         178 _log(2, "FLA Paths: @paths");
266             }
267 35         96 return @paths;
268             }
269              
270             sub _get_includes
271             {
272 288     288   451 my $checkfile = shift;
273 288         447 my $content_ref = shift;
274 288         320 my $seen_ref = shift;
275              
276 288         396 my @deps;
277              
278             # Check both ascii and ascii-unicode, supporting Flash MX and 2004 .fla files
279             # This will fail for non-ascii filenames
280 288         433 my @matches = ${$content_ref} =~ m/\#\0?i\0?n\0?c\0?l\0?u\0?d\0?e\0?(?:\s\0?)+["]\0?([^"\r\n]+?)["]/gxms; ## no critic (EscapedMeta)
  288         5665  
281 288         654 foreach my $inc (@matches)
282             {
283 62 100       236 next if ($seen_ref->{$inc}++); # speedup
284             # This is a hack. Strip real Unicode down to ASCII
285 31         166 $inc =~ s/\0//gxms;
286 31 50       76 if ($inc)
287             {
288 31         42 my $file = $inc;
289 31 50       389 if (! -f $file)
290             {
291 31 50       340 if (! File::Spec->file_name_is_absolute($file))
292             {
293 31         342 my $dir = [File::Spec->splitpath($checkfile)]->[1];
294 31 50       99 if ($dir)
295             {
296 31         810 $file = File::Spec->rel2abs($file, $dir);
297             }
298             }
299 31 50       826 return [$inc] if (! -f $file);
300             }
301 31         58 push @deps, $file;
302 31         109 _log(2, "#include $inc from $checkfile");
303             }
304             }
305 288         1195 return @deps;
306             }
307              
308             sub _get_imports
309             {
310 288     288   411 my $checkfile = shift;
311 288         361 my $content_ref = shift;
312 288         296 my $fla_path_ref = shift;
313 288         305 my $imported_file_ref = shift;
314 288         288 my $seen_ref = shift;
315              
316 288         289 my @deps;
317 288         335 my @matches = ${$content_ref} =~ m/i\0?m\0?p\0?o\0?r\0?t\0?(?:\s\0?)+((?:[^\;\0\s]\0?)+);/gxms;
  288         15260  
318 288         540 foreach my $imp (@matches)
319             {
320 283 100       997 next if ($seen_ref->{$imp}++); # speedup
321             # This is a hack. Strip real Unicode down to ASCII
322 221         553 $imp =~ s/\0//gxms;
323 221         665 _log(2, "import $imp from $checkfile");
324 221         395 my $found = 0;
325 221         248 foreach my $dir (@{$fla_path_ref}, as_classpath())
  221         463  
326             {
327 331         3340 my $f = File::Spec->catdir(File::Spec->splitdir($dir), split m/[.]/xms, $imp);
328 331 100       1362 if ($f =~ m/[*]\z/xms)
329             {
330 220         1132 my @d = File::Spec->splitdir($f);
331 220         316 pop @d;
332 220         1243 $f = File::Spec->catdir(@d);
333 220 100       3773 if (-d $f)
334             {
335 110         389 my @as = grep { m/[.]as\z/xms } File::Slurp::read_dir($f);
  220         8536  
336              
337 110         210 for my $file (@as)
338             {
339 220         614 $imported_file_ref->{$file} = 1;
340             }
341 110         245 @as = map { File::Spec->catfile($f, $_) } @as;
  220         2187  
342              
343 110         190 for my $file (@as)
344             {
345 220         705 _log(2, " import $file from $checkfile");
346             }
347 110         251 push @deps, @as;
348             }
349 220         648 $found = 1;
350             }
351             else
352             {
353 111         189 $f .= '.as';
354 111 100       2797 if (-f $f)
355             {
356 110         327 my @p = split m/[.]/xms, $imp;
357 110         241 $imported_file_ref->{$p[-1] . '.as'} = 1;
358 110         304 _log(2, " import $f from $checkfile");
359 110         161 push @deps, $f;
360 110         128 $found = 1;
361 110         245 last;
362             }
363             }
364             }
365 221 100       651 return [$imp] if (!$found);
366             }
367 287         1110 return @deps;
368             }
369              
370             sub _get_instantiations
371             {
372 288     288   356 my $checkfile = shift;
373 288         386 my $content_ref = shift;
374 288         320 my $fla_path_ref = shift;
375 288         315 my $imported_file_ref = shift;
376 288         341 my $seen_ref = shift;
377              
378             # Get a list of all classes defined in this file
379 288         380 my @classes;
380 288         314 my @class_matches = ${$content_ref} =~ m/c\0?l\0?a\0?s\0?s\0?(?:\s\0?)+((?:[^;\s\0]\0?)+)/gxms;
  288         5859  
381 288         542 for my $class_match (@class_matches)
382             {
383 233         358 $class_match =~ s/\0//gxms;
384 233         544 push @classes, $class_match;
385             }
386              
387 288         442 my @deps;
388 288         307 my @matches = ${$content_ref} =~ m/n\0?e\0?w\0?(?:\s\0?)+((?:[\w.]\0?)+)[(]/gxms;
  288         5281  
389 288         565 foreach my $imp (@matches)
390             {
391 211 100       856 next if ($seen_ref->{$imp}++); # speedup
392             # This is a hack. Strip real Unicode down to ASCII
393 180         371 $imp =~ s/\0//gxms;
394 180 100       445 next if ($exceptions{$imp});
395 156         518 _log(2, "instance $imp from $checkfile");
396 156 100       442 next if ($imported_file_ref->{$imp . '.as'});
397             # Is this class implemented in this very file?
398 132 50   79   868 next if any { $_ eq $imp || m/[.]\Q$imp\E\z/xms } @classes;
  79 50       643  
399 132         495 my $found = 0;
400 132         153 foreach my $dir (@{$fla_path_ref}, as_classpath())
  132         340  
401             {
402 132         1694 my $f = File::Spec->catdir(File::Spec->splitdir($dir), split m/[.]/xms, $imp);
403 132         413 $f .= '.as';
404 132 50       3487 if (-f $f)
405             {
406 132         420 _log(2, " instance $f from $checkfile");
407 132         192 push @deps, $f;
408 132         168 $found = 1;
409 132         198 last;
410             }
411             }
412 132 50       393 return [$imp] if (!$found);
413             }
414 288         996 return @deps;
415             }
416              
417             =item $pkg->as_classpath()
418              
419             Returns a list of Classpath directories specified globally in Flash.
420              
421             =cut
422              
423             sub as_classpath
424             {
425 353 50   353 1 748 if (!$cached_as_classpath)
426             {
427 353         576 my $prefs_file = flash_prefs_path();
428 353 50 33     1108 if (!$prefs_file || ! -f $prefs_file)
429             {
430             #_log(2, 'Failed to locate the Flash prefs file');
431 353         770 return q{.};
432             }
433              
434 0         0 my $conf_dir = flash_config_path();
435 0         0 for (File::Slurp::read_file($prefs_file))
436             {
437 0 0       0 if (m/(.*?)<\/Package_Paths>/xms)
438             {
439 0         0 my $cp = $1;
440 0         0 my @dirs = split /;/xms, $cp;
441 0         0 for (@dirs)
442             {
443 0 0       0 if (!$conf_dir)
444             {
445 0         0 _log(2, "Failed to identify the UserConfig dir for '$_'");
446             }
447             else
448             {
449 0         0 s/[$][(]UserConfig[)]/$conf_dir/xms;
450             }
451             }
452 0         0 $cached_as_classpath = \@dirs;
453 0         0 _log(2, "Classpath: @{$cached_as_classpath}");
  0         0  
454 0         0 last;
455             }
456             }
457             }
458 0         0 return @{$cached_as_classpath};
  0         0  
459             }
460              
461             =item $pkg->flash_prefs_path()
462              
463             Returns the file name of the Flash preferences XML file.
464              
465             =cut
466              
467             sub flash_prefs_path
468             {
469 353     353 1 600 return _get_path('pref');
470             }
471              
472             =item $pkg->flash_config_path()
473              
474             Returns the path where Flash stores all of its class prototypes.
475              
476             =cut
477              
478             sub flash_config_path
479             {
480 0     0 1 0 return _get_path('conf');
481             }
482              
483             =item $pkg->set_verbose($boolean)
484              
485             =item $pkg->set_verbosity($number)
486              
487             Changes the verbosity of the whole module. Defaults to false. Set to
488             a number higher than 1 to get very verbose output.
489              
490             The C environment variable sets this at module
491             load time.
492              
493             The default is C<0> (silent), but we recommend setting verbosity to
494             C<1>, which emits error messages. Setting to C<2> also emits
495             debugging messages.
496              
497             =cut
498              
499             sub set_verbose
500             {
501 0     0 1 0 my $pkg = shift;
502 0         0 my $new_verbosity = shift;
503              
504 0 0       0 $pkg->set_verbosity($new_verbosity ? 1 : 0);
505 0         0 return;
506             }
507              
508             sub set_verbosity
509             {
510 1     1 1 2 my $pkg = shift;
511 1         3 my $new_verbosity = shift;
512              
513 1 0       6 $verbosity = !$new_verbosity ? 0
    50          
514             : $new_verbosity =~ m/\A (\d+) \z/xms ? $1
515             : 1;
516 1         3 return;
517             }
518              
519             =item $pkg->get_verbosity()
520              
521             Returns the current verbosity number.
522              
523             =cut
524              
525             sub get_verbosity
526             {
527 0     0 1 0 my $pkg = shift;
528 0         0 return $verbosity;
529             }
530              
531             # Internal helper for the above two functions
532             sub _get_path
533             {
534 353     353   446 my $type = shift;
535              
536 353         740 my $os = $os_paths{$OSNAME}; # aka $^O
537 353 50       730 if (!$os)
538             {
539 353         709 return;
540             #croak "Operating system $^O is not currently supported. We support:\n ".
541             # join(q{ }, sort keys %os_paths)."\n";
542             }
543 0         0 my $list = $os->{$type};
544 0         0 my @match = grep { -e $_ } @{$list};
  0         0  
  0         0  
545 0 0       0 if (0 == @match)
546             {
547 0         0 return;
548             #croak join("\n ", 'Failed to find any of the following:', @{$list})."\n";
549             }
550 0         0 return $match[0];
551             }
552              
553             # A simplified version of Module::Build::Base::up_to_date
554             sub _up_to_date
555             {
556 299     299   368 my $src = shift;
557 299         351 my $dest = shift;
558              
559 299 50       3708 return 0 if (! -e $dest);
560 299 100       9420 return 0 if (-M $dest > -M $src);
561 288         655 return 1;
562             }
563              
564             sub _log
565             {
566 1508     1508   2754 my ($level, @msg) = @_;
567              
568 1508 50       2970 if ($verbosity >= $level)
569             {
570 0         0 print @msg, "\n";
571             }
572 1508         2676 return;
573             }
574              
575             1;
576             __END__