前些天写了一个通过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个:
- 用-a参数将整个Encode模块都打包进去。(测试不可行)
- 弃用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就会报错,这一点也浪费了我不少时间。