前些天写了一个通过HTTP API 读取公司LDAP信息,并写入到数据库的Perl脚本。因为希望做到在没有Perl环境的Windows Server上都能运行,于是尝试用社区神器PP(Par::Packer)打包成exe。

打包结果很令人满意,在本机上测试也完全没有问题,但令人沮丧的是,在某些Server上测试的时候出现The locale codeset(CPxxx) isn’t one that perl can decode错误。

可能因为出现这个问题的概率比较小,Stackoverflow 和 PerlMonk 上都没有人解释出现这个问题的原因,也没有提供一个理性的解决方案,于是仔细研究了一下,发现了一些PP的Tutorial上没有讲清楚的特性。

打包所在机器的环境

  • OS:Windows 2003 SP2 32bit English
  • Perl环境:StrawBerry 5.16.3.1
  • PP版本:1.014

某测试机运行时的错误信息:

The locale codeset (cp936) isn't one that perl can decode, stopped at Encode/Locale.pm line 94.
Compilation failed in require at LWP/UserAgent.pm line 1001.
Compilation failed in require at script/bpinfo.pl line 2.
BEGIN failed--compilation aborted at script/bpinfo.pl line 2.

提示信息的意思很清楚,Perl的Encode::Locale模块无法解码CP936(简体中文)编码集,而且和LWP::UserAgent|Encode::Locale模块相关。仔细看了一下代码之后发现调用关系是这样的:

我要通过公司内部的HTTP API获取JSON数据并解析,所以用到了 LWP::Simple 这个模块,LWP::Simple调用了 LWP::UserAgent 中的 env_proxy,而该方法会在运行时调用Encode::Locale分析环境变量字符的编码,然后通过Encode::decode解码。

但是事实上Encode模块是完全能够解码cp936的,那为什么会出现这种错误呢?

分析了以后发现了症结在于,Encode::Locale是在运行时调用的,如果在Windows平台上运行,会通过chcp命令获取当前的编码集设定,而P在打包时如果使用了-x参数,pp会在运行时分析需要的模块并打包。这样,如果在cp1252(拉丁文编码集)的环境下运行pp,Encode中用于decode简体中文的模块(Encode::CN)就不会被打包。所以生成的exe文件在正常的简体中文Windows下会在运行的时候获取到cp936编码集,但是无法Decode。

LWP::UserAgent->env_proxy 方法:

sub env_proxy {
    my ($self) = @_;
    require Encode;
    require Encode::Locale;
    my($k,$v);
    while(($k, $v) = each %ENV) {
	if ($ENV{REQUEST_METHOD}) {
	    # Need to be careful when called in the CGI environment, as
	    # the HTTP_PROXY variable is under control of that other guy.
	    next if $k =~ /^HTTP_/;
	    $k = "HTTP_PROXY" if $k eq "CGI_HTTP_PROXY";
	}
	$k = lc($k);
	next unless $k =~ /^(.*)_proxy$/;
	$k = $1;
	if ($k eq 'no') {
	    $self->no_proxy(split(/\s*,\s*/, $v));
	}
	else {
            # Ignore random _proxy variables, allow only valid schemes
            next unless $k =~ /^$URI::scheme_re\z/;
            # Ignore xxx_proxy variables if xxx isn't a supported protocol
            next unless LWP::Protocol::implementor($k);
	    $self->proxy($k, Encode::decode(locale => $v));
	}
    }
}

Encode::Locale初始化时用于获取Windows编码集的代码片段:

if ($^O eq "MSWin32") {
	unless ($ENCODING_LOCALE) {
	    # Try to obtain what the Windows ANSI code page is
	    eval {
		unless (defined &GetACP) {
		    require Win32::API;
		    Win32::API->Import('kernel32', 'int GetACP()');
		};
		if (defined &GetACP) {
		    my $cp = GetACP();
		    $ENCODING_LOCALE = "cp$cp" if $cp;
		}
	    };
	}

	unless ($ENCODING_CONSOLE_IN) {
	    # If we have the Win32::Console module installed we can ask
	    # it for the code set to use
	    eval {
		require Win32::Console;
		my $cp = Win32::Console::InputCP();
		$ENCODING_CONSOLE_IN = "cp$cp" if $cp;
		$cp = Win32::Console::OutputCP();
		$ENCODING_CONSOLE_OUT = "cp$cp" if $cp;
	    };
	    # Invoking the 'chcp' program might also work
	    if (!$ENCODING_CONSOLE_IN && (qx(chcp) || '') =~ /^Active code page: (\d+)/) {
		$ENCODING_CONSOLE_IN = "cp$1";
	    }
	}
    }

想到的解决办法有2个:

  1. 用-a参数将整个Encode模块都打包进去。(测试不可行)
  2. 弃用LWP::Simple,直接 use LWP::UserAgent,且不调用env_proxy方法。

第一个方法经过测试之后发现不可行,原因也很简单,pp打包时根据需要生成了几个和encode相关的dll文件,于是在其他机器上运行时,真正起作用的时那几个dll,所以依然会出错。

第二个方法测试可行。虽然这样无法获取系统的代理设置(LWP::Simple则默认调用env_proxy),但是需要从环境变量中获取代理设定的情况几乎没有,所以可以说没有影响。

据说一些匿名和尚说,使用ActiveState家的PDK打包不会出现这种错误,但是这货卖几百美刀,实在太贵,我也不想用破解版,何况还是搞清楚问题的来龙去脉比较好。

PP这个玩意虽然很神,但是问题不少,比如它虽然提供了一个给exe加图标的选项,但是我用的时候会报The system cannot execute the specified program错误,最后发现它只支持单一分辨率的icon(比如32bit 16×16),使用内置多种格式的icon就会报错,这一点也浪费了我不少时间。