Showing posts with label Tech. Show all posts
Showing posts with label Tech. Show all posts

Wednesday, July 09, 2008

We're moving to procrastiblog.com

I'm moving the blog to a new URL and a new host.* Future posts will appear at



The new feed is http://procrastiblog.com/feed/. The existing archives have been imported to the new site.

* One thing shouldn't require the other but—despite some dedicated users on the forums (or actually, just this one guy)—Blogger showed a perfect indifference to getting procrastiblog.com up on their servers. I'll give WordPress a chance to squander my money for a while.

Sunday, June 29, 2008

OCaml's Unix module and ARGV

Be warned: the string array argument to Unix.create_process et al. represents the entire argument vector: the first element should be the command name. I didn't expect this, since there is a separate prog argument to create_process, and ended up with weird behavior* like,


# open Unix;;
# create_process "sleep" [|"10"|] stdin stdout stderr;;
10: missing operand
Try `10 --help' for more information.
- : int = 22513

This can be a bit insidious—in many cases skipping the first argument will only subtly change the behavior of the child process.

Note that the prog argument is what matters in terms of invoking the sub-process---the first element of the argument vector is what just what is passed into the process. Hence,

# create_process "gcc" [|"foo";"--version"|] stdin stdout stderr;;
- : int = 24364
foo (GCC) 4.2.3 (Ubuntu 4.2.3-2ubuntu7)


* Actually, this "weird behavior" is the test that finally made me realize what was going on. The emergent behavior of my app was much more mysterious...

Tuesday, June 24, 2008

Resetting a Terminal

You tried to cat a binary file and now your terminal displays nothing but gibberish? Just type reset (it may look like ⎼␊⎽␊├).

It has taken me more than 10 years to learn this.

[UPDATE] Interestingly, this doesn't work in my (alas, ancient) Mac OS X 10.3.9 terminal. Any tips? Also, why did curl URL_TO_BINARY hose my terminal in the first place?

Monday, June 23, 2008

Ripping a Muxtape

So Muxtape is a pretty cool site, but a little frustrating. If a friend posts a really cool mixtape (maybe you know somebody who just barely entered the Aughties), it would be nice to be able to download it and save it, just like all those old cassette mixtapes sentimentally rotting underneath your bed.

Enter muxrip. This simple Ruby script takes the name of the mixtape, downloads it, and creates a playlist for you in M3U or iTunes format. (Acknowledgments: the script basically just adds some polish to this previous effort.)

PLEASE: Use this script responsibly. It would be a shame for Muxtape to get shut down.

ALSO: I wouldn't be surprised if this suddenly stopped working. It depends on elements of the page layout and URL scheme that might (almost certainly will) change without notice.

Sunday, June 08, 2008

Tweaking an RSS Feed in Python

I've been teaching myself a bit of Python by the just-in-time learning method: start programming, wait for the interpreter to complain, and go check the reference manual; keep the API docs on your hard disk and sift through them when you need a probably-existing function. Recently, I wanted to write a very simple script to manipulate some XML (see below) and I was surprised (though it has been noted before) at the relatively confused state of the art in Python and XML.

First of all, the Python XML API documentation is more or less "go read the W3C standards." Which is fine, but... make the easy stuff easy, people.

Secondly, the supposedly-standard PyXML library has been deprecated in some form or fashion such that some of the examples from the tutorial I was working with have stopped working (in particular, the xml.dom.ext module has gone somewhere. Where, I do not know).

So, in the interest of producing more and better code samples for future lazy programmers, here's how I managed to solve my little problem.

The Problem: Twitter's RSS feeds don't provide clickable links

The Solution: A script suitable for use as a "conversion filter" in Liferea (and maybe other feed readers too, who knows?). The script should:


  1. Read and parse an RSS/Atom feed from the standard input.

  2. Grab the text from the feed items and "linkify" them

  3. Print the modified feed on the standard output.


Easy, right? Well, yeah. The only tricky bit was using the right namespace references for the Atom feed, but again that's only because I refuse to read and comprehend the W3C specs for something so insignificant. I ended up using the lxml library, because it worked. (The script would be about 50% shorter if I hadn't added a command-line option --strip-user to strip the username from the beginning of items in a single-user feed and a third shorter than that if it only handled RSS or Atom and not both.)

Here's the code, in toto. (You can download it here.)

#! /usr/bin/env python

from sys import stdin, stdout
from lxml import etree
from re import sub
from optparse import OptionParser

doc = etree.parse(stdin)

def addlinks(path,namespaces=None):
for node in doc.xpath(path,namespaces=namespaces):
# Turn URLs into HREFs
node.text = sub("((https?|s?ftp|ssh)\:\/\/[^\"\s\<\>]*[^.,;'\">\:\s\<\>\)\]\!])",
"<a href=\"\\1\">\\1</a>",
node.text)
# Turn @ refs into links to the user page
node.text = sub("\B@([_a-z0-9]+)",
"@<a href=\"http://twitter.com/\\1\">\\1</a>",
node.text)

def stripuser(path,namespaces=None):
for node in doc.xpath(path,namespaces=namespaces):
node.text = sub("^[A-Za-z0-9_]+:\s*","",node.text)

parser = OptionParser(usage = "%prog [options] SITE")
parser.add_option("-s", "--strip-username",
action="store_true",
dest="strip_username",
default=False,
help="Strip the username from item title and description")
(opts,args) = parser.parse_args()

# For RSS feeds
addlinks("//rss/channel/item/description")
# For Atom feeds
addlinks( "//n:feed/n:entry/n:content",
{'n': 'http://www.w3.org/2005/Atom'} )

if opts.strip_username:
# RSS title/description
stripuser( "//rss/channel/item/title" )
stripuser( "//rss/channel/item/description" )
# Atom title/description
stripuser( "//n:feed/n:entry/n:title",
namespaces = {'n': 'http://www.w3.org/2005/Atom'} )
stripuser( "//n:feed/n:entry/n:content",
namespaces = {'n': 'http://www.w3.org/2005/Atom'} )

doc.write(stdout)


If there are any Python programmers in the audience and I'm doing something stupid or terribly non-idiomatic, I'd be glad to know.

Thanks in part to Alan H whose Yahoo Pipe was almost good enough (it doesn't handle authenticated feeds, as far as I can tell) and from whom I ripped off the regular expressions.

[UPDATE] Script changed per first commenter.

Wednesday, April 30, 2008

Linux Quickies


The upgrade from Ubuntu Gutsy to Hardy Heron (cool logo, right?) was relatively uneventful. Some minor points...


  • I always thought the main Ubuntu servers would farm my downloads off to an appropriate mirror, but apparently that's not the case. You're likely to get better download times if you choose a mirror in System -> Administration -> Software Sources. If you choose "Other...", there's a "Select Best Server" feature. Oddly, my best response times were from New Zealand... maybe because they were all asleep when I tried it.


  • The "ugly fix" for the infamous hard disk annihilating bug stopped working after I upgraded. This new, different (but still ugly) fix worked for me. It would be really great if the Ubuntu team could find a way to make the OS stop trying to kill my hard disk by default.


  • My WiFi light stopped working after the upgrade. This is very easily fixed by installing the package linux-backports-modules-hardy.


  • etckeeper is a great idea: it puts all the config files in /etc under Git, Mercurial, or Bazaar source control and forces APT to commit before and after any upgrade, so it's easy to isolate and revert changes. (As a side note, using Bazaar for a few weeks makes it physically painful to be forced to deal with CVS.)


  • Anti-aliased fonts in Emacs are really nice. On Ubuntu Hardy, install emacs-snapshot-gtk (on prior releases, downloads "Pretty Emacs"), then run emacs-snapshot instead of emacs (or run update-alternatives to set emacs-snapshot as the default). You should then be able to run, e.g., emacs --font "Monospace-10" and get pretty, pretty (lick-able, as they say) fonts. Other reasonable choices are "BitstreamVeraSansMono-X" or "LiberationMono-X", where X is your desired point size. You can also invoke M-x set-default-font and type your choice interactively, but for some reason the TrueType fonts above won't tab-complete—if you type a non-existent font, Emacs will silently use the default system fixed-width font (see System -> Preferences -> Appearance -> Fonts). I've added the following to my .emacs:

    (if (>= emacs-major-version 23)
    (set-default-font "Monospace-10"))

    (The conditional is necessary if you may come into contact with earlier versions of Emacs, which will barf on TrueType fonts.)


  • In my experience, the fonts in your web browser will look better if you don't use Microsoft's gratis TrueType core fonts (package msttcorefonts in Ubuntu/Debian). In particular, the Trebuchet font (which crops up frequently, including at the top of this page) tends to look pretty bad with subpixel rendering turned on. Red Hat's Liberation fonts (package ttf-liberation) are designed as drop-in replacements for the Microsoft fonts, but I haven't seen much value in installing them.


  • The instructions I gave last month for hooking up to a projector aren't complete, because they often won't let you run the projector at a resolution greater than 640x480. This led to a rather embarrassing scene in front a class of undergraduates, where OpenOffice.org simply refused to operate at such a pathetic resolution. This problem can be solved by the methods presented here, though it requires a bit of tweaking to get things just so. I haven't yet discovered a minimal solution—first I need to crack the meaning of the X11 "MetaModes" option. When I do, you'll be the first to know.

Tuesday, April 15, 2008

Only Thus Can It Be Unmade

The cleverer among you will espy the problem below immediately


$ export DATE=`date`
$ echo $(DATE)
bash: DATE: command not found

In my half-caffeinated state, it took several minutes of frustration to figure out what was wrong: $(DATE) is a Make-style variable; in Bash, $(DATE) is the same as `DATE` (a command substitution). The correct token is $DATE.

$ echo $DATE
Tue Apr 15 11:08:38 EDT 2008

I apologize for inflicting my stupidity upon you.

Wednesday, April 02, 2008

Using an External Monitor or Projector With My Linux Laptop

For years, it was difficult enough to get my laptop working with an external monitor that I didn't even bother trying: I would boot into Windows in order to give a presentation. (This is the only reason I ever booted into Windows (or have a Windows install).) It either got dramatically easier to accomplish this at some point in the last year, or I've been incredibly stupid all this time. Just in case, here's how it works on my Dell Inspiron 6400 running Gutsy. My video card is an NVIDIA GeForce Go 7300


  1. Plug in the external monitor or projector. The monitor may work immediately (especially if you're repeating this step after fiddling about below), but it may be at the wrong resolution.


  2. Open "Applications -> System Tools -> NVIDIA Settings" or execute sudo nvidia-settings on the command line. This utility is provided by the nvidia-glx-new package, which you should probably have installed.


  3. Choose "X Server Display Configuration" and click "Detect Displays" at the bottom of the screen.


  4. The external monitor should appear in the Layout pane. Click on it, then click "Configure". Choose "TwinView" (which should hopefully not say that it requires an X restart).


  5. In the "Display" box, choose "Position: Clones". This means that you want the same display to appear on both monitors. This is what works best for me, particularly for giving presentations. Having separate displays seems to confuse applications—for example, "Presentation Mode" in Evince will "center" the slides, displaying the left half of a slide on the right half of the laptop screen and the right half of a slide on the left half of the projector. It's probably possible to tweak this with exactly the right viewport/workspace settings (ugh), but that's not how I roll.


  6. If the display is smaller than the default display—the display's square will be smaller in the Layout pane and the displayed area will be cropped on the screen—click on the
    default display in the Layout pane and choose a lower resolution. 1024x768 is usually safe. The laptop display will probably look bad, but the external display should look fine.

    Be careful: any smaller than 1024x768 and the Settings applet will be too big to display on the screen. If this happens, you'll have to navigate blind or hit Ctrl-Alt-Backspace to restart X (or don't automatically hit OK after the resolution changes and it will revert after 15 seconds).



To remove the external monitor or projector:


  1. Unplug the monitor.

  2. Click "Detect Displays".

  3. A message "The display device FOO has been unplugged..." will appear. Click "Remove."

  4. Click "Quit".



Under no circumstances should you click "Save to X Configuration File" at any point in this process. That's just asking for trouble.

Some sequence of actions—it's not clear which—may screw up the "X Server Display Configuration" pane. The display will
continue to function in the meanwhile, but all the above commands are inaccessible. Restarting X made it go away (for me).

[UPDATE] It seems it's necessary to update your xorg.conf to get decent resolution on some projectors. I'm still investigating... In the meantime, this should help.

Monday, March 24, 2008

LaTeX Appendectomies

I have need of a LaTeX package. I think a lot of people would find this package useful. I would prefer not to write it myself.

This package would take a mode argument in the preamble and format the document in one of three ways: as a conference submission, as a camera-ready conference paper, or as a tech report.

Suppose I have a theorem and that theorem has a proof.


  • In a conference submission, the theorem would appear in the main text and would be re-stated along with its proof in an appendix.

  • In a camera-ready conference paper, the theorem would appear in the main text and the proof would not appear at all.

  • In a tech report, the theorem and the proof would appear inline in the main text.


Preferably, proofs could be included in the main text or sent to an appendix on a case-by-case basis. Proofs could also have "sketch" versions and full versions: the sketch version appears in the main text of a conference paper (either kind) and the full version appears only in a tech report.

Suppose that, in proving a theorem, I first prove a lemma.

  • If the proof of the theorem appears in the main text (or an appendix), then the lemma and its proof should also appear in the main text (or the appendix), before the theorem.

  • If the proof of the theorem is omitted, or if a proof sketch is included which makes no reference to the lemma, then the lemma and its proof should not appear at all.



One should be able to conditionally include text depending on the mode. For example, in camera-ready conference mode, one would probably include the sentence: "Full proofs of all theorems appear in a technical report [citation here]."

The only package I've found that does anything like this is thrmappendix , but it doesn't allow for a proof to appear in the main text at all. It's primarily concerned with the appearance and re-appearance of the theorem, with or without its proof; I'm primarily concerned with the appearance or suppression of the proof.

Thursday, January 24, 2008

The Triumphant Return of C-c C-t

The upgrade to Ubuntu gutsy and/or Emacs 22 broke my favorite feature of tuareg/ocaml-mode: C-c C-t for "show type" in OCaml buffers (this requires compiling with -dtypes, which generates type annotation files). I suffered without this for a length of time which is either embarrassing or impressive, depending on whether you consider poking around inside Emacs Lisp files a productive or unproductive use of time...

I finally broke down and fixed it today. The problem is simply that Emacs and OCaml packages aren't cooperating properly. My solution, which may or may not be optimal, is as follows:


  1. Copy the directory /usr/share/emacs/site-lisp/ocaml-mode to a path of your choosing, say ~/.emacs.d/emacs22/ocaml-mode. Let's call this directory DIR
  2. (Optional) In Emacs 22, execute C-u 0 M-x byte-recompile-directory and choose DIR.
  3. Add the following line to your .emacs file:
    (or (< emacs-major-version 22) (push "DIR" load-path))



The test for whether it worked is: load a .ml file and type C-c C-t. In the mini-buffer, you'll either see "type: ..."; "Point is not within a typechecked expression or pattern"; or "No annotation file..." If it says "C-c C-t is undefined", then you have failed.

Tuesday, January 15, 2008

Eye of the Tiger

Does anybody know which new API in Mac OS X 10.4 is the reason I can't use iLike the Amazon MP3 Downloader on my Power Mac G4? Any can anybody tell me why it sucks?

Believe it or not, I actually can't upgrade to 10.4, because it only comes on DVD-ROM and my, ahem, 6 year old G4 doesn't have a DVD-ROM drive. (You can get CDs if you buy a copy of 10.4 and send Apple a check for ten or fifteen bucks, but... eh, no.) I will not be buying a new computer this year.

Monday, November 26, 2007

xkcd: Success

This is pretty much exactly how it went down when I upgraded to Gutsy.

Success

Consider this a standing endorsement of xkcd.

Sunday, November 25, 2007

Gnome Sessions

I tentatively clicked "Remember current running applications" in Gnome Session Preferences (aka gnome-session-properties) and lived to regret it. What this does is it restarts any currently running application when you login. This is useful for, e.g., your online backup daemon, but kind of annoying for, e.g., five Emacs windows, Last.fm, some random Nautilus directory window, etc.

Now, first I tried checking and unchecking "Automatically remember running applications when logging out", as the window layout makes it seem as if these two settings are related. They are not. Then, I was tempted to fix this by futzing with the "Startup Programs" or "Current Session" lists. This is Not Right.

The Right Thing is to close all your programs (or just the offending ones) and then click again on "Remember current running applications". That is to say: the only way to change the "remembered" snapshot is to take another snapshot*.

Note: Session Preferences has a Help button, but the Gnome manual page on it doesn't mention "Remember currently running programs" or "Automatically remember running applications when logging out". This is annoying.

* Presumably there is a text file tucked away somewhere that controls this (maybe ~/.gnome2/session?), but I haven't the patience to find out.

Style Guidelines for People

In the midst of some unrelated Googling, I came across Luca de Alfaro's style guidelines for student co-authors. This is good stuff. I particularly like "one sentence per line" b/w "fill-sentence macro". It's an elegant solution to a frequently annoying deficiency of diff, which is unfortunately the baseline for anyone collaborating via CVS or SVN. I tweaked his macro to get nice indentation in AucTeX:


(defun fill-sentence ()
(interactive)
(save-excursion
(forward-char)
(forward-sentence -1)
(indent-relative)
(let ((beg (point)))
(forward-sentence)
(if (equal "LaTeX" (substring mode-name (string-match "LaTeX" mode-name)))
(LaTeX-fill-region-as-paragraph beg (point))
(fill-region-as-paragraph beg (point))))))
(global-set-key "\ej" 'fill-sentence)


[UPDATE 1/20/07] Fixed an off-by-one error when the cursor is on the first character of the sentence by adding (forward-char).

LaTeX Letters

I was trying to write a letter in LaTeX the other day:


\documentclass{letter}

\address{Nowheresville}

\signature{Me}

\begin{document}
\begin{letter}

\opening{To Whom It May Concern:}

Hello, there.

\closing{Sincerely,}

\end{letter}
\end{document}

This led to the following two errors, which shed little light on the situation:

! LaTeX Error: There's no line here to end.

See the LaTeX manual or LaTeX Companion for explanation.
Type H for immediate help.
...

l.10 \opening{To Whom It May Concern:}

and (on a different example)

! Incomplete \iffalse; all text was ignored after line 66.

\fi
l.16 \end{letter}

Runaway text?
\@mlabel{}{\unhbox \voidb@x \ignorespaces \global \let

The problem, as it was gently explained to me, is I had omitted the second mandatory argument of \begin{letter}, which is the address of the recipient. The following is correct:

\documentclass{letter}

\address{Nowheresville}

\signature{Me}

\begin{document}
\begin{letter}{Foo Corp.}

\opening{To Whom It May Concern:}

Hello, there.

\closing{Sincerely,}

\end{letter}
\end{document}


[UPDATE] I just realized that the reason I got so confused about this is that I was working off a previous business letter that was formatted like:

\begin{document}
\begin{letter}
{
Foo Corp. \\
... \\
ATTN: Warranty Dept.}
...

I'm not sure if I intended it to be the case (probably not), but LaTeX picked up the braces around the address as the argument to letter. When I used this as the template for a personal letter and deleted the address, all hell broke loose.

Thursday, November 15, 2007

Fake project directories in tarballs

To make a tarball where all the files are in a subdirectory FOO (as per best practices), where FOO doesn't really exist on your disk (e.g., FOO may be PROJECT-vX.Y.Z and the files are in directory PROJECT), just do


tar cvf NAME.tar --transform=s,^,FOO/,g FILES

Note that the argument to transform in this case is just a sed command with commas instead of slashes.

Saturday, November 10, 2007

Eye Candy

The difference between "Desktop Plane" and "Desktop Wall" in the Ubuntu "Visual Effects" options (aka CompizConfig Settings) is that the latter allows windows to overlap a viewport* and the former does not. (Along with this comes a lot of incidental options and visual fillips, like the ability to drag a window entirely from one viewport to another.) Although this does not sound like a big productivity booster, I'm going to give the Wall a chance.

I'm not going to give the "Desktop Cube" a chance, because it won't let me place viewports above and below, as well as left and right, seemingly out of some wrong-headed sense of pseudo-three-dimensional literalism (although your "cube" can have an arbitrary number of faces, they must be arranged linearly from left to right: Euclidian topologies only).

* For some reason the "Desktop Plane," "Desktop Wall," and "Desktop Cube" options all use viewports and not workspaces**, so they don't work well with the Gnome Workspace Switcher.

** For some other reason, Gnome has two distinct ways of implementing virtual desktops (viewports and workspaces) even though theres no discernible advantage to one over the other (except for compatibility with this application or that).

[UPDATE] Visual Effects lead to intermittent system freezes. Fun! Going back to boring old workspaces.

Tuesday, November 06, 2007

Updating for Daylight Savings Time on Ubuntu

My system clock has been all wiggy since Daylight Savings Time ended (or started?) (it's ended) on Sunday. Believe it or not, it was actually flipping back and forth between correct and one hour ahead for no apparent reason. I did two things which together seem to have fixed things.

First, via Fast Track Sites, I found that my system timezone information was out of date. The test for this is:


% sudo zdump -v /etc/localtime | grep 2007
/etc/localtime Sun Mar 11 07:59:59 2007 UTC = Sun Mar 11 01:59:59 2007 CST isdst=0 gmtoff=-21600
/etc/localtime Sun Mar 11 08:00:00 2007 UTC = Sun Mar 11 03:00:00 2007 CDT isdst=1 gmtoff=-18000
/etc/localtime Sun Nov 4 06:59:59 2007 UTC = Sun Nov 4 01:59:59 2007 CDT isdst=1 gmtoff=-18000
/etc/localtime Sun Nov 4 07:00:00 2007 UTC = Sun Nov 4 01:00:00 2007 CST isdst=0 gmtoff=-21600

If the output doesn't exactly match the above, you have a problem. Download the latest tzdata2007X.tgz file (where X is a lowercase letter) from the National Cancer Institute (seriously). For gorey details, see the Fast Track Sites post cited above. (I don't think you really have to do the ln step, which sets your timezone to EST5EDT instead of, e.g., America/New_York. I skipped it.)

Now your system ought to know the right start/end dates for Daylight Savings Time. But your clock is probably still out of whack.

Now, via Ubuntu Forums and Stephen Sykes, use ntpdate to reset the clock. The trick(s) here are: (a) you have to shut down ntpd first, (b) setting the clock back an hour will convince sudo that you're trying to do something nefarious ("timestamp too far in the future"), and (c) I had to give ntpdate the -u option to get past some unseen firewall.

% sudo /etc/init.d/ntp-server stop
* Stopping NTP server ntpd [ OK ]
% sudo ntpdate-debian -u
6 Nov 17:00:00 ntpdate[13693]: step time server 66.36.239.104 offset -3598.042737 sec
% sudo /etc/init.d/hwclock.sh restart
sudo: timestamp too far in the future: Nov 6 17:59:56 2007

Oops. Using the "Adjust Date & Time" applet, manually set the clock one hour forward. Now, run sudo -k. Now, set the clock back to the correct time (again using "Adjust Date & Time"). Starting over:

% sudo ntpdate-debian -u
6 Nov 17:00:00 ntpdate[13693]: step time server 66.36.239.104 offset -3598.042737 sec
% sudo /etc/init.d/hwclock.sh restart
* Saving the system clock
% sudo /etc/init.d/ntp start
* Starting NTP server ntpd [ OK ]


All done. Enjoy.

[UPDATE 3/12/2008] It looks like this might be a semi-annual ritual: my system pulled the same schizo act when DST started this week. On Gutsy, ntp-server has become ntp. It's easier springing forward than falling back, because sudo just times out when you set the clock forward.

Tuesday, July 24, 2007

A Dubious Assertion

Being a conscientious software engineer, I try to be good about putting assert statements in my code. And being a verification guy, I find myself tempted to express fairly deep correctness properties in my assertions. And this fills me with such satisfaction, that I am such a wise and clever programmer, that I should do such things.

But then I'm trying to optimize some code so that it runs in something like an acceptable amount of time and for some reason I just can't shake this routine out of its stupor... What's going on here?

Don't add assertions that change the asymptotic complexity of your algorithm. That's just dumb. (Of course you can always compile your code with assertions turned off, but even in testing the difference between O(n) and O(1) can pinch.)

And now look at how much more wise and clever and self-satisfied I can be.

Monday, July 09, 2007

Changing your PATH in Emacs' compilation mode

[UPDATE: This is not really wrong, but not really right either. See below.]

I was a bit surprised at this problem, but I suppose most people use standard make or gcc to build... I want to build my project with a version of OMake that I have compiled and installed in my home directory. I have ~/tools/bin in my PATH, but for some reason M-x compile still gives me

/bin/bash: omake: command not found

The trick is that Emacs invokes the compile command in a non-interactive, non-login shell, which means that neither your .bash_profile nor your .bashrc (or any variations thereof) are going to get read.* The workaround is to set BASH_ENV to point to a script file that sets your PATHbash reads the file pointed-to by BASH_ENV in non-interactive mode. Here's my solution:

~/.bash_profile:
. ~/.bashrc

~/.bashrc:
export BASH_ENV=~/.bash_env
. "$BASH_ENV"

~/.bash_env:
export PATH=/home/chris/tools/bin:$PATH

There's probably a good reason why this is a bad idea, but it works.

* A quick refresher course: .bash_profile is for login shells; .bashrc is for interactive, non-login shells; BASH_ENV is for non-interactive, non-login shells (which, confusingly, will probably be a sub-process of an interactive and/or login shell, which is why the above example works).

[UPDATE] The compilation shell being non-interactive and non-login is a red herring. While this is certainly the case, a non-interactive, non-login shell will inherit the environment of it's parent process. So, for instance, if your PATH is properly set in your shell and you invoke Emacs from the command line, things should be fine.

What was really causing my problem is that I was invoking Emacs from the Gnome Panel. The environment that Emacs inherits in this case is Gnome's, not Bash's. How do you change the PATH in the Gnome environment? Um... Eh... gnome-session-properties? .gnomerc?

The solution I've settled on is to create a .xsession file as follows,
~/.xsession:
#! /usr/bin/bash

if [ -f ~/.bash_env ]; then
. ~/.bash_env
fi

exec gnome-session

where .bash_env is as above.

NOTE: If you leave off the last line, your X session will end before it begins. The .xsession script is the X process: when it ends, the X process ends. Execing gnome-session replaces the script process with the Gnome session process.

[UPDATE 2] Of course, another option is to just use setenv in your .emacs file. TMTOWTDI, in Emacs and Perl alike.