howto
Writing CPAN modules
it's hard
Oh, you wanted to write portable CPAN modules?
First: Decide what you mean by "portable".
- I used to think I could write perl code that will run everywhere perl does.
- But look at the platform coverage of CPAN smoke tests:
lots of Linux, some FreeBSDs, Darwins, a few MSwin32s, a little Solaris...
- My current take: if the VMS community wants us to support it,
they should set-up a smoke test machine.
- Also note that while the smoke tests are a great feature
it does take a long time to get all the results back: a long dev cycle.
- Also: decide how you feel about dependencies
- If your target audience is using automated dependency
tracking (CPAN.pm, CPANPLUS.pm, or perhaps things like apt-get using
re-packaged cpan code) then there are no worries.
- If you want to support people who are manually
installing each module, then you need to worry about adding a
dozen obscure dependencies.
- (This is the kind of thing that makes Module::Install look good...)
- Probably: you should avoid using non-core modules that only provide
small conveniences (List::MoreUtils, Test::Differences...).
Aren't there simple Best Practices you can follow for portability?
- Read perldoc perlport
And prepare to be confused.
- It says something like "Perl is already portable, so don't worry so much!"
Then it goes on for twelve thousand words about things to worry about.
- And it's not necessarily up-to-date, either.
File paths
- Standard advice: you should use File::Spec
rather than manipulate file paths directly.
- And now there's Path::Class
which can do it even more neatly.
- But: I almost never use File::Spec... and yet my code gets through the smoke tests.
Pop quiz. Does this look like portable code?
my $cmd = "locate seat_of_pants 2>&1";
my $results = qx{ $cmd };
As far as I'm concerned it is. It can fail if there's no "locate" command,
but that bourne-shell style io-redirect is okay. Certainly it works on windows:
shell redirect on windows, qx?
I have had better luck with backticks than messing around with IPC::* modules.
Quoting perlport:
In general, don't directly access the system in code meant to be portable. That means, no "system",
"exec", "fork", "pipe", "``", "qx//", "open" with a "|", nor any of the other things that makes being
a perl hacker worth being."
It appears that that's out-of-date... at least, given my critereon for portablity.
You might think you can write cross-platform code something like this:
if ( $^O =~ /win/ ) {
# do it the windows way
} elsif ($^O =~ /VMS/ {
croak "I give up";
} elsif ($^O =~ /.../ {
# ...
} else {
# unix-like code
}
-
There are many problems here.
- First of all, we blew it on the /win/ pattern (which also matches "cygwin", and "Darwin").
- But more importantly: no one out there is really maintaining capabilities databases
for all these platforms.
- I don't even think there's a definitive list for all values of $^O.
(Though many are in perlport, under PLATFORMS).
-
- It makes more sense to try a plug-in system that looks for subclasses named with $^O
- That way, if you want it to work on your platform, it's up to you to find someone using that platform to get the plugin working.
- One module that works this way is File::Spec. (Follow that link to get an idea of what a mess cross-platform programming can be.)
-
- And these days, $^O reports "MSwin32" on nearly every windows platform.
- It could be there's a slight difference between Windows95 and Vista.
- The core module Win32 has a GetOSName()
you can use to distinguish sub-types of "MSWin32".
- So your plug-ins might need plug-ins. (David Sharnoff has a solution for that:
Plugins. "Plugins allows plugins [to] have plugins and for all the plugins to share a single configuration file.")
-
Let's say that you want to use an external program
if it's available on the system. How would you find out?
- A good idiom is to try it and see if it works, then fall back
to something else if it's not available.
- This is a simple, reasonably robust way of checking for an
external program:
sub can_run_program {
my $program = shift;
my $devnull = File::Spec->devnull;
my $found = qx{ which $program 2>$devnull } ||
qx{ $program --version 2>$devnull };
return $found;
}
internationalization
Once upon a time (5.8 era), I wanted to get capitalize_title()
working with at least the European characters....
- The solution (?): use locale
-
The pragma use locale; seemed like a simple solution
-
It took the hint from my locale setting: en_US.iso8859-1
- uc() would then know how to turn a ü into a Ü
-
Can you write tests that depend on locale settings?
- You can't know in advance if locale is going to be "C" or "en_US.iso8859-1" or something else
entirely.
-
You can set it with the POSIX module's "setlocale"...
-
The tests for PerlIO::locale do something like this:
use POSIX qw(locale_h);
SKIP: {
setlocale(LC_CTYPE, "en_US.UTF-8") or skip("no such locale", 1);
open( my $fh_out, ">", "foo") or die $!;
print $fh_out "\xd0\xb0";
close $fh_out;
open(my $fh_in, "<:locale", "foo") or die $!;
is(ord(< $fh_in $gt;), 0x430);
close $fh_in;
}
- But...
-
- Can you assume you're on a POSIX system? (The
"perlport" docs call that "a pretty big assumption"...)
- Is it common to have a "en_US.UTF-8" available,
or will the test just get skipped a lot?
-
Can you even assume that
"en_US.UTF-8" is what the locale will be called? Locale names
aren't actually standardized that well (!):
See perldoc
perllocale look under "Finding Locales".
But if you take a look at the smoke test results for PerlIO::locale,
you'll see that he's largely gotten away with these assumptions:
PASS (166) FAIL (31) NA (13) UNKNOWN (18)
What I actually did, though, for the
Text::Capitalize
tests is I got scared by
the above issues and wrote operational tests... if it looks like uc() knows
what to do with a few international characters, then I go ahead with
those test cases, if not, then I skip them.
Something like this:
SKIP: {
skip( "Can't test strings with international chars", 1 ) unless i18n();
my $result = capitalize_title( $case );
is ($result, $expected, "test: $case");
}
sub i18n {
$lower = 'ü';
$upper = 'Ü';
if ( ($upper eq uc($lower) ) &&
($lower eq lc($upper) ) ) { # transformed as expected
return 1;
}
return 0;
}
Recently, I noticed that these tests were always getting skipped, even on
my own box.
- use locale ignores UTF-8 locales
- It turns out: I'm now living a UTF-8 life: locale, editor, terminal are
all now UTF-8 rather than iso8859-1
- When your locale is en_US.UTF-8, the pragma use locale no
longer has any effect on the behavior of uc() and friends (!?).
- If you want things like uc() to work right, you need to do a
utf8::upgrade() on each variable that you expect to use "uc()" on.
Conclusion: perl internationalization is still a mess.
But wait. there's more... If you want to write portable perl code,
you need to know what sort of input and output encodings the user is
expecting.
- portable encodings
-
What if you want your scripts to be able to output international characters
in the encoding that the user expects when you don't know that encoding in
advance?
use PerlIO::locale;
binmode STDOUT, ":locale";
- And what if you want this to work with a "Test::More" based script?
-
You need to do this:
use PerlIO::locale;
my $builder = Test::More->builder;
binmode $builder->output, ":locale";
binmode $builder->failure_output, ":locale";
binmode $builder->todo_output, ":locale";
-
So this is fine... Provided you're not worried about those FAILs
on the PerlIO::locale smoke tests. Does doing this improve
portability or hurt it? It's your guess...
punt with private tests
Writing tests is a good thing, but there are very important
things you probably can't test.
One of my modules is intended to work with Postgresql.
Does the system have postgresql installed? Do you have an
account and password to access it (maybe via a .pgaccess file?).
Do you have permissions to do a CREATE DATABASE?
How about a DROP DATABASE?
In practice: be willing to punt.
You can write private tests that you won't ship with
the packages: you just don't include them in the MANIFEST...
(and don't use Module::Install).
Another good trick: ship a test that skips itself unless it's run with a special option.
Some examples to look at:
Next:
ExtUtils::MakeMaker vs. Module::Build vs. Module::Install
Joseph Brenner,
22 Sep 2009