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.

Awesome! Had to do a
sudo gem install builder(maybe that should be included in the tutorial?) first but then everything worked perfect.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?
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
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
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
Sam and Rafael:
I'll take a look at this and get back to you next week. Thanks for trying it out!
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.
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
just search for and install freetype and not freetype2.
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
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:
and install like this:
http://darwinports.com/
hope that helps
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
I'm not sure if this the a problem, but my swfmill is located /usr/local/bin/swfmill
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!!
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
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
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_assetstask (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
-mxflag 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.
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
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?
also, how do you go about debugging in this setup?
Sam, check this logger
http://www.luminicbox.com/blog/default.aspx?page=post&id=2
Hmm, that sounds like a good idea for a post... ;)
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.
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
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.
Yeah, that's accurate.
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.
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.
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:
You'll have to put this statement at the top of the class file:
Thx a lot that was the problem.
Sam: AFAIK swfmill doesn't import sound files. You're gonna have to load them dynamically.
Ok Ben thanks, would you have a general idea or can you point me to how to load them dynamically?
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
got it to work, school boy error really. I copied the code into textmate, the quotes " ” were screwed. After replacing them it all works!!
Hello again, Have you or do you use remoting with this setup? amfphp? If so do you have an example. Thanks for any help
Remoting is pretty straightforward. Get the source files from Adobe's website, drop them into your classpath, and proceed as normal.
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.