March 28, 2008
Creating Cross Platform Windows and Mac Installer CDs
I'm working on an Adobe AIR cross-platform app at work. We're providing a physical CD for distributing the application and I wanted the CD to work like a real Mac CD should look like -- Background image in the finder, hide all extraneous files, auto-open the finder window when the CD is inserted, etc.
Getting just this to happen requires some trickery, and getting it to happen while also putting an auto-launching Windows installer on the same disc proved to be even more of a game.
After a lot of googling I think I've got a pretty good process down and wanted to share, so here are the details.
Core Concepts
- The standard ISO CD filesystem doesn't let you do all the good stuff that you'll want to do for Mac (finder background image, hiding files, etc). You need to use some variant of HFS. However, Windows can't read HFS.
- 'Hybrid CDs' solve the previous problem. From what I understand, a hybrid CD contains an HFS volume and a standard ISO volume. Windows machines can't see the HFS volume and OS X machines ignore the ISO volume when there is an HFS volume present.
- You'll need the OS X Developer tools for these steps
- From what I understand, Roxio Toast 7 & above make this much easier, but this is more fun and it's free. :)
- It is much easier to test all this out in Windows if you have Parallels (or presumably Fusion) because Parallels let's you mount an iso in Windows as if it were a CD, so you don't have to waste your precious blank CDs creating actual copies. :)
Steps
Create a standard folder on your harddrive and put all the install files (windows and mac, including an autorun.inf for your Windows installer, presumably) in it. We'll call it 'install folder' for now.
Make a user-friendly finder window
To make a nice OS X finder window for a CD (or dmg or whatever) we want to have a friendly background image that helps the user understand what they're supposed to do. Go grab AdiumX to see an example of what I mean. We have to include this image on the CD, but we also want to hide it from the end user so that it doesn't clutter up our install window. Here are the steps:
- Create your background image.
- Create a folder called 'background' inside 'install folder'.
- Put your background image into this folder.
- Use terminal to rename this folder to '.background'. e.g.: mv background .background (The folder should then disappear from view in the Finder)
- Open 'install folder' in the Finder and hit Command+J. Make sure that 'This window only' is selected. Click 'Picture' for the background. In the dialog that comes up, hit command+shift+g to bring up a box where you should enter the path to your '.background' folder. This allows us to navigate there even though it is hidden. Select your background image and click OK.
- Click 'Picture' again for the background. For some reason it gets reset after selecting the picture.
For each file that you'd like hidden in both windows & mac (e.g. perhaps the window autorun file), run this command in Terminal:
/Developer/Tools/SetFile -a V /path/to/your/folder/name-of-file-to-hide
Get rid of the finder toolbar (click that top-right oval button) and resize your window & arrange your icons, leaving the windows icons clumped together somewhere for now (we'll hide them in a minute)
Create the hybrid ISO using hdiutil Run this command in Terminal to turn your folder into a hybrid ISO:
hdiutil makehybrid /source/folder/name/ -o outputfile.isoMount the ISO in read-write mode: Run this in terminal:
hdiutil attach -readwrite outputfile.isoMake files invisible to Mac ONLY (e.g., your windows installer file) by setting the invisible bit on them: Again in terminal:
/Developer/Tools/SetFile -a V /path/to/your/folder/name-of-file-to-hideMake the finder window auto-open when the cd is inserted/image is mounted: In Terminal:
sudo bless -folder "/Volumes/discName" -openfolder "/Volumes/discName"Test it out. Mount your ISO and unmount it a couple times to see the wonderous installer. If you have Parallels you can try it out in Windows too by using 'Connect Image' to simulate it as a CD in Windows
Open Disk Utility & drag your iso file into the left-hand pane. Click 'Burn' in the toolbar above. Insert a blank CD and let it burn.
Done! Try inserting it into a Mac and a Windows machine and be amazed by the beauty of it all.
November 21, 2007
Resurrecting Actionscript's 'Delegate.create' for function binding in AS3
Derek Wischusen showed a cool technique for simulating Ruby's Mixin's in Flex here. I started trying it out and was loving it until I found out some details about event listeners that were show-stoppers for me. Well, after learning more about Prototype recently (with it's very closely related javascript/ecmascript) I solved the problem and am throwing this code up here in case anyone else finds it useful.
The Problem
Derek's code allows you to add code to your classes by 'mixing' in other code. One way to add this 'other code' is by adding functions to the prototype. (see Derek's post for details) Unfortunately however, when functions are added in this way and used as event listeners, the 'this' keyword inside the function will refer to the global object instead of the object you meant it to be attached to. (see 'Programming Actionscript 3.0' pg 361 http://www.adobe.com/go/programmingAS3_pdf). This means that your listener function becomes essentially static to you and can't reference all the data you want it to.
The solution, it turns out, is to resurrect Mike Chamber's old idea of Delegate.create. This static method was used like so:
myComboBox.addEventListener("change", Delegate.create(this,onMyComboBoxChange));
And it ensured that when 'onMyComboBoxChange' was called, the 'this' keyword would refer to the 'this' from the 'create' call (or whatever obejct you placed there). Delegate.create was apparently dropped in ActionScript 3.0 because they thought we didn't need it anymore (because they added method closures which are automatically bound to their object and they don't like using the prototype anymore I guess, silly them).
However, since actionscript anonymous functions are closures, you can do the same thing that the Prototype framework does (which almost the exact same code) to add a 'bind' method to the Function class so that you can get the same result as above with this code:
myComboBox.addEventListener("change", onMyComboBoxChange.bind(this));
Viola! Back to the land of having fun with Derek's mixin class.
Here is the code (someday I will extend it to be more general like Prototype's code, but for now all I need it for is simple event listeners):
Function.prototype.bind = function():Function {
var __method:Function = this;
var object:Object = arguments[0];
return function():void {
__method.apply(object, arguments);
}
}
November 9, 2007
Best Free .NET RegEx Designer
Just wanted to thank and put out a link to RadSoftware's awesome (and free) Regular Expression Designer. Best free .NET RegEx tool I've tried.
November 8, 2007
Ruby MySQL gem install problems solved
I noticed an error in my rails log today that said:
"WARNING: You're using the Ruby-based MySQL library that ships with Rails. This library is not suited for production. Please install the C-based MySQL library instead (gem install mysql)."
Unfortunately, when I went to install the gem I got an error going something like this:
Failed to build native gem extension ... * extconf.rb failed * Could not create Makefile due to some reason, probably lack of necessary libraries and/or headers.
I am using the MacPorts install of MySQL. It took a little leg work but I found a post that led me to use the following command, which installed the gem on my Intel Mac successfully.
sudo env ARCHFLAGS="-arch i386" gem install mysql -- --with-mysql-config=/opt/local/lib/mysql5/bin/mysql_config
November 6, 2007
DSLs and the 90 percent
90%, 90%, 90%!!!
Background:
Atwood on Languages Inside Languages (e.g.: Using multiple DSLs at the same time)
Rob Conery in defense of (his) ORM
Jeff, why the SubSonic hate? Rob, why the Regex hate? Why not use them where they're appropriate?
1) I want SubSonic to turn 90% of my Database needs into easy one-liners.
2) For everything else, I want SubSonic to get out of my way.
SubSonic does a pretty good job of both things.
Beautiful:
Order o = Order.FetchByID(orderID);
foreach (Product p in order.Products)
doSomeCoolThing(p.price, p.weight, p.title);
(plus intellisense and compile-time errors)
Waste-of-my-time:
IDataReader rdr = QueryDb("SELECT * FROM Products WHERE OrderID = '" + " sqlEscaper(orderID) " + "');
while (reader.Read())
doSomeCoolThing(
reader.GetFloat(reader.GetOrdinal("price")),
reader.GetInt32(reader.GetOrdinal("weight")),
reader.GetTitle(reader.GetOrdinal("title"))
);
(yikes.)
That right there is 90% of the SQL queries for 90% of DB applications out there. Look at how much more beautiful SubSonic can make the world.
Jeff is right though, people get their hands on a tool like SubSonic and suddenly think it will do their laundry and solve world hunger too. SubSonic's fluent interface doesn't cut it for complex data queries (and is often ugly when you try). But it shouldn't try to cut it, and it doesn't.
When you want to do complex data queries, drop down to SQL, or wrap your queries in Views, etc. Either way, you gotta know SQL, so don't try to hide it in a Kingdom of Nouns.
Jeff shouldn't be so down on SubSonic -- it is actually more of what he's talking about: It's a Domain Specific Language itself (for the most common queries a DB application makes). Why not let it play along with SQL & Regex too? Just don't try to use it for what it's not good at, just like with SQL & Regex.
Domain Specific Languages are fun and useful. SubSonic is fun and useful. They both need to stay out of each other's territories. Why the hate?
Last note on Regex's: When you want to parse something out of a string, use a short, concise Regex. People usually hate Regex because they get so excited by it that they try to write a single indecipherable Regex string that encapsulates the complete parsing structure of a entire programming language. Bad idea, but they blame it on Regex. You don't need an IDE for Regex's, you need to divide and conquer and separate your concerns, just like you approach every programming challenge in your favorite language: Wrap your whole program into one method and you will die. Period.
November 2, 2007
Capistrano Profile Loading Fixed
My previous post about not having profiles load is now thankfully obsolete, due to the latest Capistrano 2.1 update.
Say the notes:
No default PTY. Prior to 2.1, Capistrano would request a pseudo-tty for each command that it executed. This had the side-effect of causing the profile scripts for the user to not be loaded. Well, no more! As of 2.1, Capistrano no longer requests a pty on each command, which means your .profile (or .bashrc, or whatever) will be properly loaded on each command! Note, however, that some have reported on some systems, when a pty is not allocated, some commands will go into non-interactive mode automatically. If you’re not seeing commands prompt like they used to, like svn or passwd, you can return to the previous behavior by adding the following line to your capfile:
default_run_options[:pty] = true
October 15, 2007
Indomitable
Landon has overcome our latest attempt to constrain him again.
If there is one word to describe our little boy, it is has to be Indomitable. Normal little guys that I've seen usually cry and throw temper tantrums and get distressed when things don't go their way. Landon, on the other hand, gets very, very focused and 'fueled' when he is under duress.
Last month he climbed out of his crib (with the bar at full height) at only 13 months. In the middle of the night last night he climbed over our child gate (which was raised an extra 5 inches off the ground). He can't do these things normally. (He's tried.) But if he is very, very frustrated and mad he gets focus and adrenaline and does these things that seem like they ought to be impossible for him. He's not a scrawny guy - he's got above-average girth that he's got to vault over these restraints.
I love him with all my heart. I hope we can find a way to convince him to sleep at night that doesn't make him feel so crazy.
October 12, 2007
Visual Studio "instance failure" from SQL Connection String
I found out about the Rails-inspired Subsonic project today and am hoping it will save me from the insanity of working with databases in Visual Studio. ;)
While following the screencast I got stuck at the first compile point because I was getting this very helpful (hah!) DB connection error from Visual Studio:
Instance failure
Fortunately, a quick Google search took me to this post on Scott Hanselman's blog where I was reminded that my connection string had a double-backslash (because I had copied it from some C# code where the backslash had to be escaped) and that the (XML) Web.config file doesn't need to have backslashes be escaped.
Backslash removed, project compiling.
Here's to hoping that Subsonic has created a good imitation of Active Record. I love SQL, but rewriting simple SQL queries or maintaining or configuring an unfriendly persistence layer (or rolling & maintaining my own) is not my idea of a good time when I'm just trying to get a project done.
September 29, 2007
Safety Kids are still around
Thanks to this review, I found out that Safety Kids are still around!
I still remember these songs from when I was a kid, and I want my kids to know them too.
September 28, 2007
SSH Environment for Capistrano Rails Deployment
I decided to take the plunge to capistrano 2.0 and try using my own (OS X) machine at the same time, and I was getting path errors.
First I got a 'svn: command not found' error, so I added this to my deploy file: set :scm_command, '/opt/local/bin/svn'
That made that error go away, but then I got a 'rake: command not found' error. So I added this to my deploy file: set :rake, '/opt/local/bin/rake'
Finally I got 'No such file to load ' rubygems' and 'custom_require.rb:27: command not found' errors, and these seemed like path problems too, so I finally decided that something was really wrong. That's when I found this post. Setting the SSH environment and 'PermitUserEnvironment yes' as described made things work!
For the record, I'm currently using Capistrano 2.0, Rails 1.2.3, and Ruby 1.8.6 to serve rails on a mongrel cluser (mongrel 1.0.1 and mongrel_cluster 1.0.2). I used MacPorts for installing ruby, mysql and apache2. Here are some of the related file contents:
~/.ssh/environment
PATH=/opt/local/bin:/opt/local/lib/mysql5/bin:/opt/local/apache2/bin:/usr/local/bin:/usr/local/sbin:/usr/local/flex/bin:/bin:/usr/bin:/usr/sbin:/sbin
RUBYOPT=rubygems
Capfile
load 'deploy' if respond_to?(:namespace) # cap2 differentiator
load 'config/deploy'
config/deploy.rb
set :application, 'myapp'
set :user, 'myusername'
set :server_hostname, 'jordan.broughs.net'
set :server_home_path, '/Users/#{user}'
set :repository, 'svn+ssh://#{user}@#{server_hostname}#{server_home_path}/svn/#{application}/trunk'
set :deploy_to, '#{server_home_path}/rails/production/#{application}'
# This fixed frustrating 'sudo: no passwd entry for app' error when cap was trying
# to run the process start command as 'app' for some reason
set :runner, 'myusername'
role :app, server_hostname
role :web, server_hostname
role :db, server_hostname, :primary => true
# Not storing database.yml in svn for security purposes. Manually uploaded it as
# shared/config/database.yml.production on the server after running 'cap deploy:setup'
# and this copies it over after each code update
task :copy_database_yml, :roles => :app do
db_config = '#{shared_path}/config/database.yml.production'
run 'export PATH=/opt/local/bin:$PATH; gem environment; cp #{db_config} #{release_path}/config/database.yml'
end
after 'deploy:update_code', :copy_database_yml
Flex - Scroll/Scrollbar Issues
I learned an important lesson in Flex layout this morning.
Synopsis:
Set minHeight=0 on an inner container to get it to display scrollbars itself instead of passing on the buck to outer containers.
Here is my post on the adobe forums about this.
Detail:
The Problem: I had a bunch of content inside of 'Box B', and Box B was itself inside of 'Box A'. I wanted scrollbars to show up on Box B, but they seemed insist on showing up on Box A.
The Solution: Box B was set to height='100%', which I thought would restrict it to being 100% of the available space of its parent container. However, it turns out that 'minimum' and 'maximum' values trump any other size settings. And by default, a Box calculates its minimum height. To calculate the minimum height, it sums up its padding, all of its childrens' heights and the gaps between its children. Thus, I was telling Box B that it should be 100% of the available height of its parent container (300px), but Box B said -- "Hold on a minute, my minimum height with all these children adds up to 500px. I'm not going any smaller than 500px no matter what you tell me my percentage height should be." The solution, then, was to tell Box B, "Forget about calculating your minimum height, I'm telling you what your minimum height is, and it is 0px". After that point, Box B happily obeyed the 100% height specification and produced scrollbars when it had more content than would fit in that space.
The Code:
Here's the non-working code:
<?xml version="1.0"?>
<mx:Application xmlns:mx='http://www.adobe.com/2006/mxml'
styleName='plain'
height='100%' width='100%'
paddingTop='10' paddingBottom='10' paddingLeft='10' paddingRight='10'
backgroundColor='black'>
<mx:Box width='100%' height='100%'
paddingTop='10' paddingBottom='10' paddingLeft='10' paddingRight='10'
backgroundColor='yellow'>
<mx:TextArea height='500'
text="Try resizing the window. You'll see that the scrollbars show up on the Application instead of the yellow box." />
</mx:Box>
</mx:Application>
Here's the working code:
<?xml version="1.0"?>
<mx:Application xmlns:mx='http://www.adobe.com/2006/mxml'
styleName='plain'
height='100%' width='100%'
paddingTop='10' paddingBottom='10' paddingLeft='10' paddingRight='10'
backgroundColor='black'>
<mx:Box width='100%' height='100%'
minHeight='0'
paddingTop='10' paddingBottom='10' paddingLeft='10' paddingRight='10'
backgroundColor='yellow'>
<mx:TextArea height='500'
text="Try resizing the window. You'll see that the scrollbars show up on the yellow box now." />
</mx:Box>
</mx:Application>
About Me
-
Interests
- My wife Debbie Brough & son Landon Brough
- My brother Drew Brough
- Rollerblading
- Biking
- Walking
- Basketball
- Scheme
- Ruby
- Rails
- SQL
- OS X
- Some Awards and Recognitions
- Current employment: Animoto.com - The End of Slideshows
- Jordan Brough's LinkedIn Profile
