Using Ruby and JSFL to automate SWF compilation

If, like me, you have to work with a bunch of .fla files that have to be published regularly, then you have certainly realized that working within the Flash IDE is far from optimal.

First of all, opening an IDE to publish a file with all the clicking involved is far from the Unix philosophy that I have learned to appreciate for a long time (./configure && make && make install…).
Also, apart from navigating through menus to publish a SWF, the keyboard shortcut that performs the same operation is a little bit contrived (ALT+SHIFT+F12…).
Multiply this by a large number of .fla files, and you get an insufferable mess.

This post will introduce a method to automate SWF publishing in a certain measure by leveraging the JSFL API and the Ruby programming language.

JSFL

Adobe introduced a Javascript API to automate the Flash IDE a few years ago. Although launching the full environment is still a prerequisite to automate tasks, it can be done from the command-line, as documented by many resources on the Internet.

A basic JSFL script looks like this:

var doc = fl.openDocument("file:///C|/Users/Test/doc.fla");
doc.exportSWF("file:///C|/Users/Test/doc.swf", true);
fl.compilerErrors.save("file:///C|/Users/Test/errors.txt", false, true);
doc.close(false);
fl.quit();

This script loads a .fla file, publishes it to the given SWF file, saves the eventual errors to a text file, and exits. It must be noted that filenames have to be specified as URLs.

To run it, use the flash executable like this:

> "C:\program files\Adobe\Flash CS5\Flash.exe" build.jsfl

This is pretty neat, but as the JSFL API has no access to its outside environment, such as the command line, it is still pretty limited if one wants to create a generic script without hard-coded file paths.
So, if we cannot write generic JSFL scripts, why not generate them ?

Ruby to the rescue

Ruby is one of my favourite languages, and it shines as a general-purpose scripting language, even in Windows. In this specific case, it has all the bells and whistles needed to solve our problems. I’m using the binary distribution found at http://rubyinstaller.org/.

Let’s write a script that will generate and invoke a JSFL script dynamically, using temporary files.

The first problem is to locate the Flash executable, since we probably don’t want to depend on a specific version and user-dependant installation path. Flash simply stores its installation path in the registry, at key HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\Flash.exe. We canĀ use the win32 registry classes, available in the default ruby installation:

require 'win32/registry'

# locate Flash
flash = nil
Win32::Registry::HKEY_LOCAL_MACHINE.open('SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\Flash.exe') do |reg|
  flash = Pathname(reg[nil])
end
puts flash
# prints C:\Program Files (x86)\Adobe\Adobe Flash CS5.5\Flash.exe

Simple enough. We are using the nil value name, which represents the default value in a registry key.

Now let’s use the nice Pathname class to get the input and output filenames from the command line and transform them into absolute URLs usable by Flash:

require 'pathname'

input_filename = Pathname(File.dirname(__FILE__)) + ARGV[0]
output_filename = Pathname(File.dirname(__FILE__)) + ARGV[1]

input_url = "file:///" + input_filename.to_s.sub(':', '|')
output_url = "file:///" + output_filename.to_s.sub(':', '|')

Ruby provides a temporary file API in the tempfile module that creates files with unique names, and deletes them when the script exits. Let’s use it to create a temporary file that will live while the script runs to store the eventual compilation errors.

require 'tempfile'

errorfile = Tempfile.new(['flash', '.log'])
errorfile.close
error_url = "file:///" + errorfile.path.sub(':', '|')

We have to close the file because by default it remains open, which would prevent Flash from writing into it.

To generate a temporary JSFL script, just write it as a heredoc string with interpolated text, and then write it to another temporary file:

js = <<EOJS
var doc = fl.openDocument("#{input_url}");
doc.exportSWF("#{output_url}", true);
fl.compilerErrors.save("#{error_url}", false, true);
doc.close(false);
fl.quit();
EOJS

jsfile = Tempfile.new(['flash', '.jsfl'])
jsfile.write(js)
jsfile.close

Finally, we just have to invoke Flash and the script using system, and handle eventual errors by parsing the error log.

# Execute the file
system("\"#{flash}\" \"#{jsfile.path}\"")

# print errors and exit 1 if necessary
errorfile.open
errortext = errorfile.read
nerrors = errortext.split(/[\r\n]+/)[-1][/^\d+/].to_i
nwarnings = errortext.split(/[\r\n]+/)[-1][/, \d+/].to_i
if nerrors > 0
 $stderr.puts errortext
 exit 1
end
if nwarnings > 0
 $stderr.puts errortext
end

Note that it is pretty useful to return a non-zero exit code in case of problem if we are calling the script in the context of a Makefile.
Be careful that Flash must not be open before for this to work correctly, because in that case, the process started by ruby exits immediatly, before the publish operation even takes place. Because of this, we would not be able to read the error log and detect eventual problems.
The error text’s last line begins by the number of errors, and further is the number of warnings. Since warnings are usually important, I chose to display them without marking the build as failed. In pure Unix tradition, the script is silent if everything goes right.

The full script can be found as a Gist here: https://gist.github.com/noirotm/3ef2f82655879d5d800e.

Going further, using a Makefile

This solution is great when having one file to handle, however with multiple flash files with shared dependencies, a smarter approach is needed. Also, the script’s execution duration is pretty long due to firing and closing a complete Flash instance.

The best solution is to use a Makefile (or ant build.xml file, or a Grunt file, or a Rakefile… many solutions exist). Such a file will allow our script to be run only if there is an actual need to do so, that is, if one of the source files is more recent than the target SWF file.

Here is an example of complete Makefile to control the build of a couple of SWF files that depends on .fla files and a bunch of ActionScript files. It assumes the presence of a few tools installed by a distribution like MSYS and MinGW:

SWFCC     = ruby swfcc.rb
RM        = rm -f

# compile all SWF files
all: foo bar

# clean every generated file
clean:
	$(RM) foo.swf
	$(RM) bar.swf

foo: foo.swf

foo.swf: foo.fla foo.as common.as
	$(SWFCC) $< $@

bar: bar.swf

bar.swf: bar.fla bar.as common.as
	$(SWFCC) $< $@

Use make or make all to build all SWF files, or make foo to build only the first one selectively. make clean will delete all SWF files, allowing you to build from a clean environment.

Please note that this Makefile and the previous script are pretty basic, and could be improved in many ways. They are however in my opinion a pretty nice starting point to develop smarter solutions.


Leave a Reply

Your email address will not be published / Required fields are marked *

This blog is kept spam free by WP-SpamFree.