Hello everybody, I’ll like to start a new category of post on this site: GUAS.
The subtitle of these series will be how not to be dumbass while being cross-platform friendly.
With this series, I’ll like to bring what I’ve learned using a not so popular platform and how to properly interact with others, without being a dumbass, obtuse or something similar.
Hard coded paths
The year is 2008, 2009 is around the corner and I keep seeing people sharing scripts, building libraries for others (distributed as gems or recipes) still hard code paths in those.
We are not just talking about Windows with drive letters, but also these scripts break between distributions… change something from /usr/bin to /usr/local/bin and boom. I’ve seen that happen.
Never assume all the other users have your same OS distribution (hey, they could not have the same OS brand, kernel or whatever).
Was ‘which’ or ‘where’?
Trying to find something? Did you succeed? Sure? try again, but now on other distro. I found myself under some distros that lack which and instead provided something called where (couldn’t remember the distro name right now).
In my job (video broadcasting) we tend to never assume your environment, never trust on it since it could fail, and when it fails your company loose piles of money.
Until I found this issue, always relied on this functionality. When switched back to Windows, found there was no where, neither which, so I found myself in a complicated situation, my next point will add a picture for you.
Almost everything that is done with pure Ruby can be done on Windows, the few cases it couldn’t, need to be handled properly.
Like on previous example, there is no which functionality on Windows. You can rely on PATH or you can split ENV['PATH'] and perform the search in Ruby code, at the end, both do the same, right?
Wrong. Doing `which ruby`.chomp could look right, but it’s an expensive call. Every time you call it a new environment is spawned where bash/tsh/sh is executed and the output is captured. This context switch can be expensive, more if done several times in the life cycle of your application.
Maybe not clear enough:
I know, I’m such a freak when talking about performance…
Even if run it just 1 time, is more portable, don’t you think?
RUBY_PLATFORM, To regexp, or not.
Under certain circumstances, you want to exclude something for being executed under some platforms, for example: Windows.
One way to do it is ask the OS about your platform… wrong, this is like relying on OS specifics, as mentioned in previous point.
I’ve seen too many `uname -a`... in such useless ways… when they should have relied on RUBY_PLATFORM instead.
It is your friend, let me show you…
So subtle, but so simple change that just skip all the platform specific stuff if no darwin platform is found.
Another nice detail that noone cares about is that relying in system calls breaks cross-compilation.
For example, using MinGW on WINE to target Windows will be useless if you rely on system calls instead of RUBY_PLATFORM
So, play nice with others, you can do it.
Bundling platform specific files (that includes binaries).
Bundling share libraries, .so files or even .dll can turn the end-user life a nightmare. I remember a while back when some gem developers released a gem with a pre-built Makefile…
Yeah, just a Makefile, a simple text file that was not automatically regenerated during gem installation and caused lot of pain to many Linux users (since the gem was built on OSX and was pointing to wrong paths, wrong extensions for the shared libraries…)
Those files should be excluded from your repositories. Adding those warrant you that doing multi-platform gem releases will became a issue (cross-compilation and such) and also will add noise to mailing list from people asking for support since these extensions cannot be loaded by their Ruby interpreter.
Go figure something so simple can cause that amount of pain.
Disclaimer
No developer, neither Windows, Linux or OSX was injured during the creation of this post. Please use my comments for good, not for evil.
You can consider this series of articles as rant, or consider them as what they are: advice.
I agree that trying to keep ruby code platform neutral is a good idea. Theres more we can do to facilitate this I think – the fire brigade is a good idea for example (http://firebrigade.seattlerb.org/), in the same vein as CPAN or CRAN’s automated testing, yet unfortunately it seems to no longer be in operation.
Other examples would be using some of Daniel Berger’s excellent platform abstraction libraries (eg sys-*), and maybe integrating stuff like win32-process et al so that Process is more platform neutral.
I think your example highlights the difficultly of writing portable code (which is probably why many of us often don’t bother). I think the example won’t work on windows due to executable extensions. Maybe something like this:
def where(app)
pathext = (ENV['PATHEXT'] || ‘.COM;.EXE;.BAT;.CMD’).split(‘;’) if RUBY_PLATFORM =~ /win32/
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
fn = File.join(path, app)
if pathext
pathext.each { |ext| return fn + ext if File.exist?(fn + ext) and File.executable?(fn + ext) }
else
return fn if File.exist?(fn) and File.executable?(fn)
end
end
nil
end
Thank you Charles for your comment.
Indeed is tricky to be cross platform friendly and I believe the work from Daniel Berger (win32utils) and some other stuff should have been part of standard ruby distribution long time ago, replacing the POSIX only version they have.
The example I provided will work, but the problem is that for files like ‘gem’ and ‘gem.bat’ will find the first one and call it without extension.
On 1.8.6-p114 this wouldn’t work, on newest versions works without issues.
Other problem is that
File.executable?think everything can be executed on Windows… such a lame supportFunny fact: your regex for RUBY_PLATFORM will be part of my next article: never use win32, there is mingw also and a 64 bits version of Ruby on Windows (mswin64).
Funny, no?
True – I used to use just =~ /win/, but that broke on darwin
, so I’m currently using File::ALT_SEPARATOR, which is pretty lame too.