Windows does have symlinks

This morning’s cpan-outdated | cpanm using my 64-bit Perl 5.20.2 built with Visual Studio 2013 brought with it a spurious test failure in Mojolicious:

t\mojo\daemon.t ............................ 1/?
#   Failed test 'right script name'
#   at t\mojo\daemon.t line 83.
#          got: 'C:\Users\...\.cpanm\work\...\Mojolicious-6.06\t\mojo\lib\myapp.pl'
#     expected: 'C:/Users/.../.cpanm/work/.../Mojolicious-6.06/t/mojo/lib/../lib/myapp.pl'
# Looks like you failed 1 test of 81.

Clearly, those two paths refer to the same file, but the strings are different. The test path is constructed using:

my $path = "$FindBin::Bin/lib/../lib/myapp.pl";

where as the server’s idea of the app’s path comes from the following bit in Mojo::Server:

sub load_app {
    my ($self, $path) = @_;
    # ...
    {
        local $0 = $path = rel2abs $path;

A-ha! Library code calls rel2abs which in turn cleans up the path using canonpath. Surely, the test code, which already constructs an absolute path, should also pass it through canonpath so it is comparing apples to apples (ideally, all paths would be constructed using catpath — but, I understand the set of platforms where this kind of thing works right most of the time is quite large).

But, there is a bit of a snag: The documentation for canonpath clearly states:

Note that this does not collapse x/../y sections into y. This is by design. If /foo on your system is a symlink to /bar/baz, then /foo/../quux is actually /bar/quux, not /quux as a naive ../-removal would give you. If you want to do this kind of processing, you probably want Cwd’s realpath() function to actually traverse the filesystem cleaning up paths like this.

Plainly, the documentation does not match behavior on Windows. Which makes some sense as, at the time when File::Spec was conceived, there were no symlinks on Windows. The code that handles canonicalization on Windows is not entirely transparent to me, but the effect is plain to see:

C:\> perl -MFile::Spec::Functions=canonpath -E "say canonpath 'a/../../b/../b/../c'"
..\c

versus

$ perl -MFile::Spec::Functions=canonpath -E 'say canonpath "a/../../b/../b/../c"'
a/../../b/../b/../c

Or, using the example from File::Spec documentation:

 C:\> perl -MFile::Spec::Functions=canonpath -E "say canonpath '/foo/../quux'"
\quux

versus

 $ perl -MFile::Spec::Functions=canonpath -E 'say canonpath "/foo/../quux"'
/foo/../quux

But, Windows has had symlinks for a while now:

C:\> dir c:\opt\bin\xx*

2015-03-01  01:48 PM    <SYMLINK>      xxh32sum.exe [xxh64sum.exe]
2015-03-20  11:20 AM           132,608 xxh64sum.exe

Since Vista, one can use the mklink command to create symlinks. mklink is rather cumbersome to use because Windows has no sudo (sudowin aside), but symlinks do exist, and they are pretty convenient once you create them.

I do not know how many Windows Perl programs rely on the existing behavior of File::Spec->canonpath. I do not know whether the behavior ought to be changed, whether the costs outweigh the benefits, but, just FYI, symlinks do exist on Windows.

Now, for Mojolicious, the logical fix for the test is to apply to the path against which they are testing the same functions that are applied when an app is created. Which means, the test should apply canonpath to the path before comparing it to the one returned, even though on Unix the canonpath is idempotent.

As much as people seem to believe otherwise, the only operating systems in the world are not just variations on a single flavor of Unix.

You can discuss this post on /r/perl.