Command-line AIR support ~ September 29, 2014

Running an Actionscript project from the command-line is not something that is frequently required (and I would certainly not recommend using Actionscript as a general scripting language), but there are corner cases where it is occasionally useful to do so. For instance, I have recently been working on a recoloring system (topic for a future blog post!) stacked on top of Flump, and the tools require that we do some post-processing on the Flump output. This processing requires both parsing the Flump library (for which we already have code written in Actionscript), and the opening and exporting of SWF assets which can only be done from within Actionscript. This process does not need any buttons or display to run; it can be entirely automated, or it could be if it were easy to run Actionscript from the command-line.

You can get most of the way there by launching a compiled AIR app from the command-line. However, while that might be handy for using a script to launch a typical, visual AIR app, it has a couple of problems for our purposes. For one, the compiled app is huge (the tiny included app in this post runs ~50MB when compiled as a standalone Mac .app package). For another, you can’t write directly to stdout from AIR (which is strange, since you can access stdout, stderr and stdin in native process which you spawn).

If you happen to find yourself in the rare circumstance of needing to run a build process or art pipeline task in Actionscript, perhaps this can help you out. I’m running with the assumption that this is executed on a machine with developer tools installed, so it executes ADL directly rather than operating on a compiled AIR app. To get around the lack of stdout support, I write to a file and tail it from a bash script, wrapping the whole thing up into a package that is pretty typical for a command-line script. It does need to be executed in an environment that has windowing support, so this may not be suitable for a typical build server, but I leave that problem as an exercise for someone who actually needs to solve it.

First, we have a very simple sample app:

App.as
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class App extends Sprite {
    public function App () {
        NativeApplication.nativeApplication.addEventListener(InvokeEvent.INVOKE, invoked);
    }

    protected function invoked (event :InvokeEvent) :void {
        NativeApplication.nativeApplication.removeEventListener(InvokeEvent.INVOKE, invoked);

        var outFile :File = new File(File.applicationDirectory.nativePath + "/out.log");
        if (outFile.exists) outFile.deleteFile();
        OUT = new FileStream();
        OUT.open(outFile, FileMode.WRITE);

        // delay for testing, and to show that this can run over multiple frames if necessary
        setTimeout(function () :void {
            // simply dump each of our arguments, one per line
            for each (var arg :String in event.arguments) {
                OUT.writeUTFBytes("arg [" + arg + "]\n");
            }

            exit();
        }, 3000);
    }

    protected function exit () :void {
        OUT.close();
        NativeApplication.nativeApplication.exit();
    }

    protected var OUT :FileStream;
}

As you can see, we get access to our command-line arguments via InvokeEvent.arguments, we can easily write our output to a temporary file instead of using trace, and the app can take as long as necessary to complete (including running over multiple frames if needed).

AIR apps require an app descriptor:

airdesc.xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<application xmlns="http://ns.adobe.com/air/application/15.0">
  <id>com.foo.app</id>
  <!-- Bundle Identifier. Required. -->
  <filename>App</filename>
  <!-- Used as the filename for the application. Required. -->
  <name>App</name>
  <!-- The name that is displayed below the app icon. -->
  <versionNumber>0.1.0</versionNumber>
  <!-- Settings for the application's initial window. Required. -->
  <initialWindow>
    <!-- arbitrary, unused -->
    <width>1152</width>
    <height>768</height>
    <!-- The main SWF or HTML file of the application. Required. -->
    <content>app.swf</content>
  </initialWindow>
</application>

This descriptor is just putting forth minimal effort to actually launch the app with ADL.

Finally, a bash script to run the whole thing:

app-run.sh
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/bash

pushd `dirname $0` > /dev/null

# empty the current file out if it exists, otherwise create it for tailing
echo -n "" > out.log
# Start tail in the background
tail -F out.log &
TAIL_PID=$!

# start ADL, passing in our command-line arguments
adl airdesc.xml -- $@

# give it a second to make sure we caught all the output
sleep 1
# Now that ADL is done, kill tail, cleanup and exit
disown $TAIL_PID
kill -9 $TAIL_PID
rm out.log

popd > /dev/null

We have to do a little bit of process gymnastics to make this act like a typical command-line script. Primarily, this just means launching the tail process in the background, then waiting for ADL to complete before performing some cleanup.

The bin/ directory that contains the script should also contain the AIR descriptor XML file, and the compiled SWF containing App.as. That’s all there is to it! Hopefully people don’t need something like this very often because it’s a nasty hack and isn’t a good solution to very many problems, but if you have something that absolutely must run in Actionscript as close to headless as possible, there it is.


comments powered by Disqus