HOWTO: Develop Flash on Mac OSX with Rake, MTASC, SWFMill and TextMate

Posted by Ben Jackson Mon, 02 Jan 2006 19:32:00 GMT

After reading my last post on this subject, a few readers asked me to write up some more details of my Flash build process, especially that spiffy TextMate command for previewing my application without leaving the editor.

What you'll need

  • Ruby. My scripting language of choice. Simple, readable, and stupidly easy to write and debug.
  • SWFMill. Generates swfs, letting you import graphics and fonts.
  • MTASC. Muchos kudos to Nicolas Canasse, the man who gave ma back my nights and weekends by writing an SWF compiler that runs much faster than the one that ships with Flash MX 2004.
  • TextMate. My text editor of choice. This is optional, and you'll only need it for the last part of the tutorial.

The Rakefile

For those of you who've never worked with Ruby or Rake (probably most), this is gonna take a little effort. Trust me that it will not be time wasted. If you've never used Ruby and want to check it out, Why's Poignant Guide to Ruby is a good place to start. If you know Ruby but have never used Rake, Martin Fowler has a great article that covers the basics through the more advanced stuff. If you want to use my code without understanding how it works, be my guest (at your own peril). Either way, to start, download the source files, take a look at the file named Rakefile and let's get cracking. Update: make sure to run sudo gem install builder first (thanks Martin).

require "rake"
require "rake/clean"
require "builder"

First, I import the Ruby libraries I'll need. The first two lines are standard issue for any Rakefile, while the last line imports the XML::Builder library, which I'll use to spit out the necessary XML for SWFMill.

CLASSPATH = ["classes", "lib/std"]
CONTROLLER_PATH = "classes/com/mydomain/Controller.as"
MAIN_SWF = "public/main.swf"
SWF_VERSION = 7
SWF_WIDTH = 300
SWF_HEIGHT = 200
SWF_BACKGROUND = 0x000000
SWF_FRAMERATE = 34
LIBRARIES = ["main"]

Next, I define my constants. This will make things much easier on me when, for example, I decide to change the dimensions of my movie, or add a new classpath. I put all of my project-specific classes (i.e. the ones I'll only use once and then throw away) in the /classes folder, while any reusable classes go in the/lib folder. This is mainly just to keep myself organized. The /lib/std folder contains the standard Flash classes like MovieClip, TextField, etc. that come with MTASC.

def mtasc klass, swf, options=nil
  input_swf = swf
  if nil != options and nil != options[:input_swf]
    input_swf = options[:input_swf]
  end
  unless File.exists? input_swf
    open(input_swf, 'a').close
    header = "-header #{SWF_WIDTH}:#{SWF_HEIGHT}:#{SWF_FRAMERATE} "
  end
  cmd = "mtasc #{klass} -swf #{input_swf} "
  CLASSPATH.each do |cp|
    cmd += "-cp #{cp} "
  end
  if options and options[:input_swf]
    cmd += "-out #{swf} -keep "
  end
  cmd += "-main " if nil != options and options[:main]
  cmd += header if header
  sh cmd
end

Next come a couple of utility functions to make my code a little cleaner. This function is just a wrapper for the MTASC command with some extra parameters. Most importantly, it supports an optional input SWF, which will come in handy when we start using SWFMill to import graphics before compiling our classes. Type mtasc --help in the terminal for more information on the different options.

@scanned_files = []
@files = []

def get_dependencies file
  @scanned_files.push file
  classes = []
  # get list of classes from file
  toplevels = []
  CLASSPATH.each do |cp|
    FileList["#{cp}/*"].each do |path|
      toplevels.push File.basename(path, ".as")
    end
  end
  open(file).each_line do |line|
    break if line =~ /^class/
    if line.match(/^import +([\d_a-z.]+).+$/i)
      classes.push $1.gsub(".", "/") + ".as"
    end
    if line.match(/((?:#{toplevels.join("|")}).+?)(/i)
      classes.push $1.gsub(".", "/") + ".as"
    end
  end
  # check against classpath
  classes.each do |klass|
    CLASSPATH.each do |classpath|
      path = classpath+"/"+klass
      if File.exists? path
        @files.push path
        if !@scanned_files.include? path 
          @files.concat get_dependencies(path)
        end
        break
      end
    end
  end
  [file].concat(@files.uniq).uniq
end

Explaining this function would be a whole other post in and of itself, so I'm just gonna wave my hands for now. Basically, it looks in file, finding all its imported classes, and their imported classes, and so on. I had to do a fair amount of voodoo to keep the universe from caving in on itself if you have two classes that import each other, so forgive me if it ain't as pretty as you'd like.

task :default => [ :main ]

desc "Build main"
task :main => :import_assets
FileList["public/main.swf"].each do |src|
  file src => get_dependencies("#{CONTROLLER_PATH}")
    mtasc CONTROLLER_PATH, src, :main => true, :input_swf => "assets/libraries/main.swf"
  end
  task :main => src
end

Ah, at last! We can do something! This is where the magic happens. First, we set the default task. This is what will be run if you just type "rake" in the terminal. Next we define the main task. This builds our main SWF with MTASC. A little background:

Rake is what's known as a dependency-based build language. All that really means is that you define a set of rules for determining if a block of code needs to be run or not. The two magic keywords in rake are task and file. Tasks can depend on other tasks, or on files, while files can only depend on other files. Generally you'll define a task to be dependent on one or more files. In this case, we define the main task to depend on all of the files that our controller class depends on, using the function defined above. We also make it depend on the import_assets task so that we know we have all of our assets in the SWF before compiling (see below). The call to FileList.each is a convenience, used to avoid repeating the path to the source. See Martin's explanation if you're still confused.

task :import_assets 
LIBRARIES.each do |swf_name|
  if File.exists?("assets/libraries/#{swf_name}")
    target = "assets/libraries/#{swf_name}.swf"
    src = Dir["assets/libraries/#{swf_name}/**/*.{jpg,swf,gif,png}"]
    file target => src do |t|
        xml = Builder::XmlMarkup.new :indent => 2
        xml.instruct! :xml, :version=>"1.0", :encoding => "iso-8859-1"
        xml.movie :version => SWF_VERSION, :width => SWF_WIDTH, :height => SWF_HEIGHT, :framerate => SWF_FRAMERATE do
            xml.frame do
                xml.library do
                    src.each do |file|
                        xml.clip :id => file, :import => file
                    end
                end
            end
        end
        assets_xml = "assets/libraries/#{swf_name}.xml"
        assets_swf = "assets/libraries/#{swf_name}.swf"
        open(assets_xml, "w") do |f|
          f.write xml.target!
      end
        sh "swfmill simple #{assets_xml} #{assets_swf}"
        open(".clobber", "a") do |f| 
          f.write "#{assets_xml}\n#{assets_swf}"
        end
    end
    task :import_assets => target
  end
end

Much respect to our budding young intern, Pedro, for hacking this out for me while I was crunched on a deadline. This task is complicated and I'm tired, so I'm gonna wave my hands again and just say that it looks at all the files in assets/libraries/main, writes the corresponding XML to import them using their path as a linkage ID, and runs SWFMill on the result. Take a look at the XML::Builder documentation if you're curious about how it works. The part at the end of the function appends the resulting scratch files to a file called .clobber, which we'll look at below.

CLEAN.include FileList["**/*.swf"]
if File.exists? ".clobber"
  clobber_content = open(".clobber").read
  clobber_list = [".clobber"]
  clobber_content.each_line do |line| 
    clobber_list << line.delete("\n")
  end
  CLOBBER.include clobber_list.sort.reverse.uniq
end

One of the coolest things about Rake is that you can tell it which files to delete to get your project folder back to where it needs to be for a clean build. Rake::Clean defines two tasks, clean and clobber. Clean is like a light version of clobber, usually only removing the most frequently-compiled files. To let Rake know which files you want to be deleted when you call rake clean or rake clobber, just add them to the CLEAN and CLOBBER arrays. This bit of code adds all the SWF files in any folder to CLEAN, and gets a list of file paths from the .clobber file to add to CLOBBER.

Compiling and Previewing Your SWF from TextMate

Now that we've got a working Rakefile, we're all set to put the finishing touch on our build script: a command that will save all our files, run Rake from our project folder, and preview our SWF in a new window, all without leaving our text editor. As a bonus, if we get any compilation errors, we can print out a list of links that will take us back to the point in the code where the error was thrown. Here's the command:

tmp=/tmp/rake_output
touch $tmp

cd $TM_PROJECT_DIRECTORY

rake 2>&1 | grep characters | grep -v Warning | perl -pi -e "s/^(.+?):(.+?): characters (\d+?)-\d+? : (.*)\$/<a href='txmt:\\/\\/open?url=file:\\/\\/${TM_PROJECT_DIRECTORY//\//\\/}\\/\$1&line=\$2&column=\$3' onClick='closeWin()'>${TM_PROJECT_DIRECTORY//\//\/}\/\$1:\$2: \$4<\/a>/g" > $tmp

if [ -s $tmp ]

then
cat <<EOD
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Compilation Results</title>
    <script type="text/javascript" language="javascript" charset="utf-8">
    // <![CDATA[
        function closeWin() {
            self.close();
        }
    // ]]>
    </script>
</head>
<body onLoad = "init()">
EOD
cat $tmp
cat <<EOD
</body>
</html>
EOD

else
echo "<meta http-equiv='refresh' content='0; tm-file://$TM_PROJECT_DIRECTORY/public/index.html'>"
fi
  • Save: All Files in Project
  • Input: None
  • Output: Show as HTML
  • Key Equivalent: Command-Enter
  • Scope Selector: source.actionscript

The command is a pretty standard-issue bash script with a couple of twists.

First, in order to read the output from Rake for our clickable errors list, we have to read it from stderr, not sdout. To do this, we add the mystic 2>&1 | after the call to rake, instead of just the normal | character. Once we get the output, we take only the lines that have the word "characters" in them (found in MTASC error messages and warnings), exclude the warnings, and perform a stroke-inducing regular expression transformation with Perl to make the errors into links with the handy "txmt" url scheme, writing the result to a temp file. Before you run this command, make sure to cut and paste "txmt://open" into your browser's location bar, and choose "TextMate" as the helper application, or it will not work.

Next, we check to see if there were any errors with if [ -s $tmp ], which checks if the temp file is empty. If it's not, then we write some wrapper HTML to stdout, including the Javascript closeWin() function that will close the window when we click on an error, followed by the list of links and a couple of closing tags.

If there are no errors, then we write a redirect to stdout that will open our HTML file with the embedded Flash movie. In theory we could just write the file's contents instead of redirecting, but TextMate's HTML preview won't show our Flash file without this intermediate step. See Allan's response to my mailing list post for more details.

So What Next?

What's that? You work on a Mac at home, but on Windows at the office? Take a deep breath and wait for my next post, where I'll go over making this sucker work cross-platform.

35 comments | no trackbacks

Comments

  1. Martin S said 3 days later:

    Awesome! Had to do a sudo gem install builder (maybe that should be included in the tutorial?) first but then everything worked perfect.

  2. Sam said 2 months later:

    Hi, im getting this error when running rake: swfmill simple assets/libraries/main.xml assets/libraries/main.swf rake aborted! Command failed with status (127): [swfmill simple assets/libraries/main.xml a...] ./rakefile:107

    Any Ideas?

  3. Rafael Guédez said 2 months later:

    Hi, awesome tutorial. For now I work with TextMate+MTASC+SAFlashPlayer and I like try with your workflow but I'm really newbie with Ruby. I follow step by step your tutorial and I think all work fine but TextMate say "/bin/bash: line 34: meta: No such file or directory". I can't understand, the file is there and I can open it with Safari. I miss something?

    Thanks in advance

    Rafael

  4. Sam said 2 months later:

    yeah, i got the same error when copying and pasting the command. It had "curly quotes" which i had to straighten, which removed that error, however now I just get a blank movie.

    when i run rake from the terminal i still get:

    swfmill simple assets/libraries/main.xml assets/libraries/main.swf rake aborted! Command failed with status (127): [swfmill simple assets/libraries/main.xml a... ] ./rakefile:107

  5. Rafael Guédez said 2 months later:

    Hi Sam, thanks for the tip. I think that for me is not compiling with mtasc. I get the same blank window and if I put errors in the class nothing happens.

    You know some way to test if rake was properly installed?

    regards

  6. Ben Jackson said 2 months later:

    Sam and Rafael:

    I'll take a look at this and get back to you next week. Thanks for trying it out!

  7. Sam said 2 months later:

    Hi Ben, I reinstalled swfmill and all is good. Have you run into any difficulties using this process to develop? Just curious, I can't stomach using the flash ide, tried both xcode and eclipse and didn't really like either. I was just introduced to texmate and really like it.

  8. Rafael Guédez said 2 months later:

    Hi Sam, I didn't install swfmill before and I'm doing it now, how you fix the problem with the freetype2 package?

    this is what say:

    Package freetype2 was not found in the pkg-config search path. Perhaps you should add the directory containing freetype2.pc to the PKGCONFIGPATH environment variable No package freetype2 found configure: error: Package requirements (freetype2) were not met. Consider adjusting the PKGCONFIGPATH environment variable if you installed software in a non-standard prefix.

    thanks in advance

  9. Sam said 2 months later:

    just search for and install freetype and not freetype2.

  10. Rafael Guédez said 2 months later:

    thanks Sam, I already can fix the problem with freetype2 installing the last version freretype219 using Fink and later setting "export PKGCONFIGPATH=/sw/lib/freetype219/lib/pkgconfig/" before install swfmill.

    But then, I got exactly this problem http://osflash.org/pipermail/swfmill_osflash.org/2005-August/000169.html

    And for now I dont know how install all dependencies in ~/usr/ like say Dan.

    You are installing freetype using Fink? is weird, because Tom say in the comments of this blog that freetype dont have the file freetype2.pc

    http://www.enflash.org/tiki-index.php?page=install%20swfmill%20on%20tiger&comzone=hide#comments

    thanks

  11. Sam said 2 months later:

    Rafael, I did not use fink, i used darwin ports to install. In the ports collection there will be freetype1 and freetype. freetype1 is version 1.3.1 and freetype is version 2.1.9

    you can search the ports collection like this:

    port search freetype

    and install like this:

    port install freetype

    http://darwinports.com/

    hope that helps

  12. Rafael Guédez said 2 months later:

    Thanks Sam, and sorry for the insistence, but the freetype2 problem still there for me using darwinports at the moment to compile swfmill. I put by hand the binary version of swfmill in opt/local/bin and work fine using it from the Terminal (for example: > swfmill swf2xml file.swf file.xml). I got installed all: ruby, gem builder, swfmill (binary) and mtasc, but from textmate I think is not running not even mtasc from the Ben command because I put deliverate errors and still opening the web preview with nothing inside. And mtasc run fine because I use it a lot from texmate but with the SAFlashPlayer for preview the swf. I like the idea of all inside textmate and the linked errors, actually I see them like yellow tips.

    thanks

  13. Sam said 2 months later:

    I'm not sure if this the a problem, but my swfmill is located /usr/local/bin/swfmill

  14. Rafael Guédez said 2 months later:

    I discover that swfmill dont was the problem, sorry for that, was my fault.

    Ok, I can get run the rake file from Terminal, but first I hardcoded all the paths and all run fine. So, the problem was TextMate! I forgot create the project, I was using a dragged folder without project. Now I love it!

    Really thanks!!

  15. Rafael Guédez said 2 months later:

    Hello Ben,

    This is the first time that I use SWFMILL, and I notice something bad. The Macromedia Components compatibility:

    http://osflash.org/swfmill#using_components

    SWFMILL increase the size of the final swf considerably.

    Would be nice if you create the same workflow with the option to discard the SWFMILL process and only use the same swf with his own library (in my case, with the components). I'll try do that with your rake file, but maybe for you is more ease.

    thanks

    Rafael

  16. Rafael Guédez said 2 months later:

    Ben, sorry for the flow, but maybe I found the solution, what I did was only comment the line 77

    # task :main => :import_assets

    with this you think I don't will problems at the future?

    Rafael

  17. Ben Jackson said 2 months later:

    Ben, sorry for the flow

    Que isso amigo? Glad to help ;)

    MTASC works with any precompiled swf. If you compile your assets in the Flash IDE, all you have to do is remove the import_assets task (as you discovered on your own).

    HOWEVER, there is an important caveat since you're using components: any classes that are referenced in the FLA will be compiled into the resulting swf with the assets. This is no problem until you go to compile your movie with MTASC. Unless you use the -mx flag on the command line, it will overwrite the mx.* class files and FUBAR your application.

    Also, if you're extending any of the MM components you should apply the V2 Components Patch to your source files so they'll compile in MTASC. There's also a Remoting-specific HOWTO in case you're using the Flash Remoting components.

    Good luck and let me know if you hit any more snags.

  18. Rafael Guédez said 2 months later:

    Really thanks Ben!

    I'm working in a personal project using only MM components and Remoting, I developed all with TextMate+MTASC, and yes, in the rake file I did what you say, remove the import_assets task, add the -mx - frame flags and the remoting confg was already. Now all work wonderful! I love the linked errors and the web preview.

    regards

  19. Sam said 2 months later:

    Ben, I curious how your implementing the MVC architecture in your apps. I can't really get a grasp of it from the example, would you possibly have a small project I could trudge thru?

  20. Sam said 2 months later:

    also, how do you go about debugging in this setup?

  21. Rafael Guédez said 2 months later:

    Sam, check this logger

    http://www.luminicbox.com/blog/default.aspx?page=post&id=2

  22. Ben Jackson said 2 months later:

    would you possibly have a small project I could trudge thru?

    Hmm, that sounds like a good idea for a post... ;)

    also, how do you go about debugging in this setup?

    I use the LuminicBox Logger for logging, and the Xray Debugger when I can't figure out the problem from the logs or when I can't find something on the stage that ought to be visible. I even (surprise!) have a command in TxMt to open up the logger in a new HTML preview window.

  23. Sam said 2 months later:

    Sorry to be a pain in the ass, but would separating classes out like this present an problem

    classes.com.mydomain.mvc classes.com.mydomain.util classes.com.mydomain.app classes.com.mydomain.etc

    Would CLASSPATH = ["classes", "lib/std"] work or would i need to add each subdirectory?

    I've been trying to adapt your example to a very simplified mvc structure using a class or package layout like the above and it keeps crapping out on me. I'm a little confused by the import statements in your example, does every class need to import the Controller? Both UselessClass and AnotherUselessClass import the Controller and I'm confused as to why.

    Thanks

  24. Sam said 2 months later:

    Sorry, I'm a dumb ass! The problem i was having is i accidentally was extending an interface.

    Am I right in thinking that I only need to import in my controller only the classes that the controller actually creates instances of and that all other classes would be automatically included.

    For instance my controller creates an instance of SomeView and SomeModel, and both of those are extended from some base classes, I only need to import SomeView and SomeModel and my base classes are included automatically. I did some tests and this seems to be the case and I just wanted to verify this is true.

  25. Ben Jackson said 2 months later:

    I only need to import SomeView and SomeModel and my base classes are included automatically.

    Yeah, that's accurate.

    Both UselessClass and AnotherUselessClass import the Controller and I'm confused as to why.

    Funny you should ask... this was more for debugging purposes than anything else. In general it's a bad idea to make two classes so tightly coupled, but sometimes it's unavoidable. This case complicates the function that gets class dependencies, and I wanted to test for it.

  26. Stefan said 4 months later:

    Hi!

    Great tut, I like it a lot. Everything is running but I have one annoying problem. I always have to remove the compiled swf-file to view changes in my code. If I don't remove it nothing happens. For example if i trace test1 I will get the output test1. If I'm not removig the compiled swf-file and change test1 into test2 and compile my code I will get test1 again. I don't have a clue why. Any ideas? Thx.

  27. Ben Jackson said 4 months later:

    Stefan:

    Sounds like a problem in the get_dependencies function. Are you explicitly importing all of your classes at the top of the file? For example, if your class calls another class like this:

        class TestClass {
            function TestClass() {
                var anotherClass = new AnotherClass();
            }
        }
    

    You'll have to put this statement at the top of the class file:

        import AnotherClass
    
  28. Stefan said 4 months later:

    Thx a lot that was the problem.

  29. Ben Jackson said 4 months later:

    Sam: AFAIK swfmill doesn't import sound files. You're gonna have to load them dynamically.

  30. Sam said 4 months later:

    Ok Ben thanks, would you have a general idea or can you point me to how to load them dynamically?

  31. mario said 4 months later:

    I get the error "/bin/bash: line 34: meta: No such file or directory" followed the tutorial al the way. If I run the rake command in the terminal i it all works? I go to Automation, then the run command then run:

    tmp=/tmp/rake_output touch $tmp

    cd $TMPROJECTDIRECTORY

    rake 2>&1 | grep characters | grep -v Warning | perl -pi -e "s/^(.+?):(.+?): characters (\d+?)-\d+? : (.*)\$/${TMPROJECTDIRECTORY//\//\/}\/\$1:\$2: \$4<\/a>/g" > $tmp

    if [ -s $tmp ]

    then cat <<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> Compilation Results EOD cat $tmp cat < EOD

    else echo “” fi

  32. mario said 4 months later:

    got it to work, school boy error really. I copied the code into textmate, the quotes " ” were screwed. After replacing them it all works!!

  33. Sam said 4 months later:

    Hello again, Have you or do you use remoting with this setup? amfphp? If so do you have an example. Thanks for any help

  34. Ben Jackson said 4 months later:

    Remoting is pretty straightforward. Get the source files from Adobe's website, drop them into your classpath, and proceed as normal.

  35. Sam said 4 months later:

    Hi Ben, Yeah I did do that, just having trouble for some reason with the responder method not being set or recognized when I call a service. So, you do use remoting and have no issues or theoretically it should work? I'm sure it's something I'm screwing up, but just wanted to be sure. I sure by now you're regretting ever publishing your technique, but I'm grateful and appreciate your help troubleshooting.

Trackbacks

Use the following link to trackback from your own site:
http://www.unfitforprint.com/trackbacks?article_id=howto-develop-flash-on-mac-osx-with-rake-mtasc-swfmill-and-textmate&day=02&month=01&year=2006

Comments are disabled