Dr Nic

Packaging TextMate bundles in OS X DMGs

Last week Engine Yard released a CLI for their Engine Yard AppCloud. Delights such as:

ey deploy
ey rebuild
ey logs
ey ssh

Engine Yard.tmbundle

They simultaneously released a TextMate bundle to deploy, rebuild, view logs, etc using Ctrl+Alt+Cmd+E. Like all TextMate bundles, you can install it in one of two ways: via git (see the README), or via a beautiful DMG. Download it here!

Yes indeed, TextMate bundles can now be packaged up and distributed via DMGs using ChocTop!

Ruby on Rails.tmbundle

For example, the Ruby on Rails.tmbundle using a simple purple theme.

First, the Engine Yard tmbundle

To use the Engine Yard tmbundle, you first need to install and use the CLI once. Instructions at the bottom of the information page.

How to package a TextMate bundle into a DMG

ChocTop is a packaging and distribution tool originally designed only for Cocoa applications, but can now package any assets, URL links, or even the whole project folder itself. This makes it ideal for packaging TextMate bundles which have no compiled/built output to distribute (like a Cocoa application), rather the project folder itself is the distributed item (the Engine Yard.tmbundle folder in this case).

Getting started

Everything is added into your TextMate bundle project. For example, with the EngineYard bundle:

gem install choctop
cd Library/Application\ Support/TextMate/Bundles/Engine\ Yard.tmbundle
install_choctop . --tmbundle

If your tmbundle already has a Rakefile, then don’t overwrite it. Instead, inside the Rakefile, add the ChocTop configuration:

require "choctop"

ChocTop::Configuration.new do |s|
  s.add_root :position => [290, 200], :exclude => %w[appcast build .bundle .git]
  s.add_link 'http://github.com/engineyard/engineyard.tmbundle', 'GitHub', :position => [520, 200]
  s.defaults :textmate
end

For TextMate bundles the DMG magic is from the s.add_root line. The resulting DMG will include the entire project as a folder/bundle. For example, you’ll want to exclude appcast, build, .bundle (if you’re using Bundler), and .git folders.

The s.defaults :textmate provides a generic background and volume icon for a TextMate bundle DMG. See below for customising the background and volume icons. The :position coordinates above are for the generic background.

Building your DMG

To build your DMG and then view it in Finder:

rake dmg
open appcast/build/*.dmg
# or together
rake dmg[automount]

You can now share the DMG file. See below for how to upload it to a server.

Versioning

In future, it would be great to use Sparkle’s auto-update mechanism (as seen in nearly every Cocoa application). ChocTop will automatically generate the required XML feed; TextMate nor the bundle has a way to ask Sparkle to poll for it nor update itself, yet.

But, you can start versioning your DMGs today:

$ rake version:current
0.0.0
$ rake version:bump:major
$ rake version:current
1.0.0
$ rake dmg

The DMG will now have a version number.

Uploading new DMG versions

The original ChocTop was designed for Cocoa applications and included Sparkle support so your Cocoa applications automatically updated themselves when you built and uploaded a new version. I haven’t got a solution for this for TextMate bundles yet; but it seems like a good idea for the future.

Nonetheless, ChocTop still includes a rake upload task to ship new versions of your DMG to a server somewhere.

In your Rakefile, add the following config lines to the ChocTop block:

s.base_url   = 'http://some.host.com/upload/folder'
s.remote_dir = '/path/to/upload/folder'
s.host       = 'some.host.com'
s.user       = 'remote-user'

The s.base_url is the URL from where the DMG file will be found by users. Later, when I figure out how to do auto-updating of the TextMate bundles, it will also use this URL. This URL is also used to determine the host for uploading the file.

The s.remote_dir is a path on the target server that maps to the base_url. This folder must already exist; rsync cannot create it as far as I can tell. So ssh into the machine and mkdir -p /path/to/upload/folder

The latter two are optional: s.host is derived from s.base_url and s.user defaults to your current local user.

To upload the latest DMG, run:

rake upload

You can now share the URL http://some.host.com/upload/folder for people to download the DMG. A small PHP script redirects from the folder path to the DMG filename.

Customising

ChocTop allows you to customise nearly everything.

The s.defaults :textmate line is similar to the following configuration:

s.background_file = "...choctop/assets/textmate_background.jpg"
s.volume_icon     = "...choctop/assets/textmate_volume.icns"
s.icon_size       = 104
s.icon_text_size  = 12

For TextMate bundles, perhaps put customised assets into a Support/dmg folder.

A background image should include blank space for the large YourBundle.tmbundle icon and webloc URL file to your GitHub project (or other target URLs). There are no size constraints on the background image. Design something beautiful.

The volume icon is an icns file. You create this using OS X’s Icon Composer application. Start with a transparent png file and drop it into the box with the corresponding size.

For the Engine Yard tmbundle, the following configuration is used:

s.background_file = 'Support/dmg/engineyard.tmbundle.dmg.png'
s.volume_icon     = 'Support/dmg/engineyard.dmg.icns'

Additional files

If there are other files you explicitly want bundled in the DMG, say a pretty README.html or a folder of documentation, then you can specify them:

s.add_file 'README.html', :position => [50, 100]
s.add_file 'docs', :position => [100, 100], :name => 'Documentation'

Summary

ChocTop is pretty cool for bundling any set of files into a custom DMG, especially Cocoa applications and now TextMate bundles.

Hopefully one day we can have Sparkle auto-updates for TextMate bundles too.

Validate and Save your Ruby in TextMate – with secret Rubinus superpowers

In some TextMate bundles, if you save a file it will also validate the file and show any syntax errors in a tooltip. This is awesome. (e.g. JavaScript and CoffeeScript)

So I added the same thing to my Ruby.tmbundle. Install this, save a dodgy Ruby file and you’ll now see something like:

Validate and Save - No Rubinius

Rubinius superpowers

Do you think the following syntax error tooltip is more useful?

Validate and Save - Rubinius installed

Yes it lovely, and the new Ruby.tmbundle will automatically do this if it can find rbx in your TextMate’s $PATH. Yeah yeah.

If you have Homebrew installed:

brew install rubinius

Then in TextMate, add your homebrew bin folder to the $PATH.

  • Go to TextMate’s Preferences (Cmd+,)
  • Go to “Advanced”, then “Shell Variables”
  • Edit the PATH variable, and add “:/path/to/homebrew/bin”

For example, if you have homebrew installed in ~/.homebrew then you might add :/Users/drnic/.homebrew/bin

. My complete $PATH in TextMate is:

/usr/bin:/bin:/usr/sbin:/sbin:/opt/local/bin:/usr/local/bin/:/Users/drnic/.homebrew/bin

Save a dodgy Ruby file and see the beautifully helpful syntax message.

Install Ruby.tmbundle

To install via Git:

mkdir -p ~/Library/Application\ Support/TextMate/Bundles
cd ~/Library/Application\ Support/TextMate/Bundles
git clone git://github.com/drnic/ruby-tmbundle.git "Ruby.tmbundle"
osascript -e 'tell app "TextMate" to reload bundles'

TextMate easter egg: find bundle commands by key combo

I’ve dreamed of the ability to ask TextMate “what frigging bundle command/snippet is being activated by Ctrl+P or Shift+Ctrl+G?” I’ve silently pined for it. (Answer: params[:id] in Rails, and all the Git bundle commands, respectively).

Really I’m an idiot because the correct thing to do is to ask on ##textmate, “is there a way to …?” but because I figured I knew everything about TextMate I just assumed you couldn’t search for bundle commands by their key combo.

I was pairing with chendo and he had obviously stopped listening to me monologuing about how to do TDD with Shoulda and was randomly clicking things on TextMate.

He found the following:

Find bundle items by key binding

Find bundle items by key binding

Gold.

Updated image

Comments suggest that “what to do” isn’t clear above. My bad.

To get this working, there are 3 steps:

  1. Press Ctrl+Cmd+To to bring up the “Select Bundle Item” box. This feature of Textmate is awesome. You can enter the text of a bundle item and it will try to find what you are looking for.
  2. Click the magnifying glass
  3. Select ‘Key equivalent’

And here’s a picture:

My attempt at sake task management

Sake set

I’ve used sake intermittently in my workflow. It competes against me writing helper/admin scripts in my ~/ruby/bin folder. Normally, executable Ruby scripts have won. But I think I have a new solution that could make sake a permanent winner for me.

Ruby scripts are easy to create and execute. You just open new file, change the TextMate grammar to ‘Ruby’, type ‘rb’ and press TAB and you’re off and running (the ‘rb’ snippet generates #!/usr/bin/env ruby or a variation of that). You then make the file executable and BAM! you can run the script from any folder in your environment.

Sake tasks are more annoying to write. After creating a new file, you need to create the namespace and task wrappers for your functionality, such as:

namespace 'foo' do
  namespace 'bar' do
    desc "This task ..."
    task :baz do

    end
  end
end

Your task isn’t instantly executable either. After each change, you need to uninstall the task (sake -u foo:bar:baz) and then reinstall the sake file (sake -i foo/bar/baz.sake) and then run it (sake foo:bar:baz). Perhaps there’s a way to inline edit a sake task, but I can’t see it from the help options.

But once you’ve got your script installed in sake, you get all the wonders that sake provides: a named list (with summary) of tasks (sake -T) and the ability to run those tasks anywhere. Ok, that’s really only one advantage over standard Ruby scripts. But I like it. Oh, namespacing. The baz task exists in a namespace foo:bar. That’s nice too.

So to make me happy, I need a solution to the dubious “create-install-execute” process above. I also want the raw source for all my sake tasks in one place so I can fix/add/change them, reinstall them and move on with my life. I want simple.

So I’ve forked Chris Wanstrath’s empty sake-tasks repo (mine) and added some infrastructure for managing sake tasks. Of course the repo itself is the repository for my sake tasks (which includes a lot from Luke Melia), but most importantly it has a single rake task to reinstall all the tasks without any manual fuss.

The rest of this article assumes you want to have your own repository for your own sake tasks hosted on github. This paragraph is probably unnecessary, but I don’t want to be accused of not being mildly thorough.

Fork the sake-tasks repo

For thoroughness and a chance to demonstrate some gold-medal git-fu, I’ll show two ways: fork my repo and forking the original repo from Chris and pulling my stuff into yours. It’s git, it’s distributed, you can do anything.

If you want to fork my repo and skip a nifty git lesson, go to my sake-tasks repo and click “fork”. Then follow the clone instructions as you normally do when you are blatantly, systematically duplicating someone else’s hard work, using a command that will look something like:

git clone git@github.com:your-github-username/sake-tasks.git

Now, lazy man, you can skip to the next step.

If you want to flex your git-fu, then go and fork Chris’ repo instead. Again, follow the clone instructions.

empty repo from defunkt

Now take a moment to reflect on just how empty your repository is. A fine moment in open-source where you’ve essentially cloned an empty repository. Hardly worth the effort, but since Chris is a creator of github then if he creates an empty repository then who am I to disagree. Empty it shall start.

Now let’s pull in the code and tasks from my repo. My repo could be any git repo anywhere on the tubes.

One way you could pull my code into your local repository is to add my repo as a remote and then pull in the goodness:

git remote add drnic git://github.com/drnic/sake-tasks.git
git pull drnic master

This is useful if you ever plan on re-pulling from a target repo again in the future.

If you just want to pull from someone’s repo one time only, then you can merge these two lines together:

git pull git://github.com/drnic/sake-tasks.git master

If you get occasional pull requests for your projects, then the latter option is handy to know.

Your local repo is now different to your remote repo (your fork on github) so push it back to your remote:

git push origin master

Installing the sake tasks

I originally created my sake-tasks fork so I could store a git:manpages:install task. I’ve just upgraded to git 1.6 (note to self: I want an ‘upgrade to latest git version via src’ task; UPDATE the repository now includes a git:src:install task to do this) and found some instructions for installing the pre-built manpages. Then I got over excited and refactored all of Luke Melia’s git+mysql+ssh tasks in to my repo so it looked like I’d done a lot of work.

To install all the tasks, first install sake:

sudo gem install sake

Then run the install task (check below for the list of tasks to be installed):

WARNING: This will uninstall any tasks you already have by the same name.

rake install

Now, check that your sake tasks are installed:

sake -T

Gives you:

sake git:analyze:commits:flog_frequent   # Flog the most commonly revised files in the git history
sake git:close                           # Delete the current branch and switch back to master
sake git:manpages:install                # Install man pages for current git version
sake git:open                            # Create a new branch off master
sake git:pull                            # Pull new commits from the repository
sake git:push                            # Push all changes to the repository
sake git:status                          # Show the current status of the checkout
sake git:topic                           # Create a new topic branch
sake git:update                          # Pull new commits from the repository
sake mysql:dump                          # Dump the database to FILE (depends on mysql:params)
sake mysql:load                          # Load the database from FILE (depends on mysql:params)
sake ssh:install_public_key              # Install your public key on a remote server.

Sexy.

Adding new recipes/tasks

The installer rake task rake install works by assuming that each .sake file contains one sake task. This allows the rake task to uninstall the tasks from sake first, and then re-install it (sake barfs if you attempt to reinstall an existing task). Without the one-task-per-file rule, the solution would be to load all the sake tasks as rake tasks into memory. But I like one-task-per-file; it seems clean.

So, to create a task foo:bar:baz, you’ll need to add a folder foo/bar and create a file baz.sake inside it. Within that file you would then specify your task using namespace and task method calls:

namespace 'foo' do
  namespace 'bar' do
    desc "This task ..."
    task :baz do

    end
  end
end

To install new tasks or reinstall modified tasks, just run the rake task (rake install or rake).

TextMate users

The latest Ruby.tmbundle on github includes a task command that generates the above namespace/task snippet based on the path + file name. That is, inside the foo/bar/baz.sake file, make sure your grammar is ‘Ruby’ or ‘Ruby on Rails’ and then type “task” and press TAB. The above snippet will be generated ready for you to specify your task.

Summary

So now I have a single place for all my original sake source and a simple rake task to re-install the tasks if I add or modify them. And because its all in one git repo, if other people fork it and add their own tasks then I can steal them.

Using Ruby within TextMate snippets and commands

I didn’t know you could run Ruby within TextMate snippets. As a consequence, a lot of the TextMate bundles I work on either have simplistic Snippets or the advanced code is run via Commands with code pushed into explicit Ruby files in the Support folder.

But sometimes I just want a clever snippet. For example, I want the ‘cla’ snippet to use the current filename to create the default class name instead of the current ‘ClassName’ default. I want default foreign key names to be meaningful.

I’ve now figured this out (thanks to Ciaran Walsh), and …

Um, lost already? Ok, let me show you via screencast on Snippets and Commands with Ruby (QuickTime (11Mb)):


TextMate Snippets running Ruby from Dr Nic on Vimeo.

Want to learn more about living with TextMate and Ruby?

The TextMate website has a series of videos, including one by the Ruby.tmbundle’s own James Edward Gray II (JEG2).

In addition, there is the latest TextMate for Rails 2 Peepcode written by myself and spoken by Geoffrey Grosenbach. Its cheap at $9, good value at $15.50, and perhaps overpriced in the $20-$30 range. Lucky its only $9.

The snippets used throughout the video

The current Ruby.tmbundle snippet (activated via ‘cla’):

class ${1:ClassName}
	$0
end

An attempt to use regular expressions to convert the filename to a classname:

class ${1:${TM_FILENAME/[[:alpha:]]+|(_)/(?1::\u$0)/g}}
	$0
end

The final snippet, with embedded Ruby to do the heavy lifting (note: added ‘singluarize’ to the snippet):

class ${1:`#!/usr/bin/env ruby
    require 'rubygems'
    require "active_support"
    puts ENV['TM_FILENAME'].gsub(/\.rb$/, '').camelize.singularize
    `}
	$0
end

Add this to your own Ruby.tmbundle, or clone mine (which is a clone of the original subversion repo).