2011/12/26

윈도우에서 Perl어플리케이션 배포

윈도우즈에서 Perl어플리케이션을 만들어 배포하는 방법에는 PAR::Packer 모듈을 쓰는 방법 상용 패키징 툴인 Activestate사의 PerlApp등을 쓰는 방법이 있다. 하지만 패키징시 필요없는 것을 너무 많이넣어 용량이 커지거나 초기실행시 내부적인 압축해제절차에 의해 속도가 느리거나 등등 단지 하나의 실행파일을 만들지 않을 것이면 패키징 하기위해 갖추어야 하는 노력이나 비용등에서 너무 오버인거 아닌가 하는 생각이 들곤한다.

예전 부터 생각하던 패키징 방식은 필요한 Perl관련 파일만을 추려서 디렉토리구조를 그대로 가져가고 간단한 .exe형태의 launcher를 만들어 그것을 실행하면 주변 환경변수라든지 모듈경로라든지 등등을 알아서 세팅한 다음 프로그램을 실행시키는 구조이다. http://perl-node-interface.blogspot.com/2011/03/deploy-perl-application-on-windows.html 에서 설명하는 방식이 그런 방식인데 저기서도 꼭 필요한 파일만 어떻게 정확하게 추려낼 것인가에 대한 방법은 완벽하지 않다.

그러면 꼭 필요한 파일을 어떻게 추려낼것인가? 를 고민하다 Windows에서 LINUX의 strace같은 프로그램이 있다면 특정 Perl스크립트를 실행시켰을때 파일을 접근하는 시스템콜을 모두 추적하면 정말 필요한 파일만 골라낼 수 있지 않을까는 생각에 Windows에서 strace같은 기능을 하는 프로그램들을 검색해보고 일일히 테스트해 보았는데 http://www.howzatt.demon.co.uk/NtTrace/ 라는 프로그램이 제일 원하는 대로 동작을 했다. 우선 패키징 작업전에 해당 싸이트에서 적절한 바이너리를 받는다.( 본인은 Windows 7 x64를 쓰기 때문에 64bit 버젼으로 받았다.)

패키징을 테스트하기 위해서 GUI면서 적절한 의존 모듈을 포함한 Perl프로그램이 좋겠다 싶어 Win32::GUI와 OpenGL에 의존성을 가진 스크립트를 선정했다. 해당 소스는 http://www.mail-archive.com/perl-win32-gui-users@lists.sourceforge.net/msg05673.html 에 있다. 일단 패키징할 Perl스크립트를 win32_opengl.pl 파일이라고 하겠다.

이제 win32_opengl.pl 파일이 실행될때 필요로 하는 Perl관련 파일들은 뽑아내보자. Nttrace 프로그램을 이용해서

NtTrace -filter File perl win32_opengl.pl > out.txt

처럼 명령을 내리면 perl win32_opengl.pl 파일이 실행되면서 접근하는 모든 파일관련 시스템콜들이 out.txt에 저장된다.
프로그램이 떠서 잘 돌아갈때 까지 기다렸다가 접근할 파일이 이제 더 이상 없겠다싶으면 그냥 종료하면 된다. (만약에 동적으로 모듈을 로딩하거나 하는 기능이 있으면 그런 것도 추적되도록 모든 기능을 시험적으로 사용후 종료하길 추천)

이제 out.txt 파일 안을 열어 보면 다음과 같이 온갖 다양한 시스템콜들이 기록되어 있다.

NtCreateFile( FileHandle=0x8e2e8 [0x78], DesiredAccess=SYNCHRONIZE|GENERIC_READ|0x80, ObjectAttributes="\??\C:\Windows\Globalization\Sorting\sortdefault.nls", IoStatusBlock=0x8e300 [0/1], AllocationSize=null, FileAttributes=0x80, ShareAccess=1, CreateDisposition=0x1, CreateOptions=0x60, EaBuffer=null, EaLength=0 ) => 0
NtQueryAttributesFile( ObjectAttributes="\??\C:\strawberry-perl-5.12.3.0-portable\perl\site\5.12.3\lib", Attributes=0x8e308 ) => 0xc000003a [3 '지정된 경로를 찾을 수 없습니다.']
NtQueryAttributesFile( ObjectAttributes="\??\C:\strawberry-perl-5.12.3.0-portable\perl\site\lib", Attributes=0x8e308 [DIRECTORY] ) => 0
NtQueryAttributesFile( ObjectAttributes="\??\C:\strawberry-perl-5.12.3.0-portable\perl\site\5.12.3\lib", Attributes=0x8e308 ) => 0xc000003a [3 '지정된 경로를 찾을 수 없습니다.']
NtQueryAttributesFile( ObjectAttributes="\??\C:\strawberry-perl-5.12.3.0-portable\perl\site\lib", Attributes=0x8e308 [DIRECTORY] ) => 0

이제 여기서 필요한 것들은 자기가 쓰고있는 Perl의 base경로를 포함한 파일들이다. 시스템콜 추적 로그파일인 out.txt 에서 필요한 파일을 현재 디렉토리 아래의 dist란 디렉토리를 만들어 뽑아내는 make_dist.pl Perl스크립트를 만들어 보면 다음과 같다.

#!/usr/bin/env perl
use 5.010;
use strict;
use warnings;

my $perl_path = qr/C:\\strawberry-perl-5.12.3.0-portable\\perl\\/;
my %files;
open my $fh , '<', 'out.txt' or die;
while (my $line = <$fh>) {
    $files{$1}=1 if $line =~ m{($perl_path.*?)".*?=> 0$};
}   

foreach my $file (sort keys %files) {
    next if -d $file;
    my $dest = $file;
    $dest =~ s/$perl_path//;
    say "$file -> $dest";
    system(qq{echo f | xcopy /C "$file" .\\dist\\"$dest"});
}

위에서 $perl_path 변수를 자기 환경에 맞게 바꾸면 된다.

perl make_dist.pl

이라고 명령을 내리면 현재 디렉토리 아래 dist란 디렉토리가 생기면서 strawberry perl이면

bin, lib , site, vendor 같은 디렉토리구조 안에 필요한 파일들만 추려져서 뽑혀져 있을 것이다.( Win32, OpenGL을 사용하는 간단한 GUI어플의 경우 4MB가 약간 넘는 양으로, PAR나 PerlApp들이 패키징하는 용량에 비하면 아주 필요한 파일만 슬림하게 뽑혀졌음을 알 수 있다.)
이 까지 작업이 끝나면 메인 스크립트인 win32_opengl.pl 과 dist\bin 과 dist\site\bin 디렉토리의 안의 모든 파일은 dist 디렉토리로 옮기자. (빈 bin 디렉토리들은 지워도 상관없음)


이제 .exe 형태의 launcher를 만들 차례다. 하지만 .exe 형태의 launcher가 필요하지 않다면 그냥 dist디렉토리에 launch.bat 이란 적절한 batch파일로 다음과 같은 내용으로 만들고 실행하면 깔끔하게 실행된다.

set PATH=.;%PATH%
perl win32_opengl.pl

그렇지만 .exe 형태로 아이콘모양도 넣고 제공해야 뭔가 일을 제대로 하는 것 같다면 약간 험난한 길을 더 걸어야 한다. 아래와 같은 명령을 내리면 perlxsi.c 라는 파일이 만들어진다. (이 작업은 dist디렉토리에서 하면 추려진 perl로 동작하므로 dist 디렉토리에서 하지 말것!)


perl -MExtUtils::Embed -e xsinit

만들어진 perlxsi.c 파일을 열어서 상단에 #include <windows.h> 와 메인 스크립트 이름을 넣는 부분과 /* Appended code */ 아랫부분을 아래와 같이 추가하고 저장한다음
( #define SRC_NAME "win32_opengl.pl" 라인이 자기의 메인 스크립트 이름을 지정하는 것)

#include <EXTERN.h>
#include <perl.h>
#include <windows.h>

/* main script name */
#define SRC_NAME "win32_opengl.pl"

EXTERN_C void xs_init (pTHX);

EXTERN_C void boot_DynaLoader (pTHX_ CV* cv);
EXTERN_C void boot_Win32CORE (pTHX_ CV* cv);

EXTERN_C void
xs_init(pTHX)
{
 char *file = __FILE__;
 dXSUB_SYS;

 /* DynaLoader is a special case */
 newXS("DynaLoader::boot_DynaLoader", boot_DynaLoader, file);
 newXS("Win32CORE::bootstrap", boot_Win32CORE, file);
}

/* Appended code */
static PerlInterpreter *my_perl; /*** The Perl interpreter ***/
int main(int argc, char **argv, char **env)
{
    char bin_path[MAX_PATH];
    char *pos; // work string.
    int n;
    /* unshift argv for reserving the original arguments */
    argv[argc] = strdup(argv[argc-1]);
    for(n=argc; n>1; n--) {
        argv[n] = argv[n-1];
    }

    if (!GetModuleFileName(NULL, bin_path, MAX_PATH)) return 1;
    pos = strrchr(bin_path, '\\'); /* the last occurrence of '\\' */
    if (!pos) return 2;            /* not found */
    pos[0] = '\\';
    pos[1] = '\0';
    /* check for MAX_PATH limit overflow */
    if ( strlen(bin_path)+strlen(SRC_NAME)+1 > MAX_PATH ) return 3;

    strcpy(pos + 1, SRC_NAME);

    argv[1] = bin_path;
    argc++;
 
    PERL_SYS_INIT3(&argc,&argv,&env);
    my_perl = perl_alloc();
    perl_construct(my_perl);
    PL_exit_flags |= PERL_EXIT_DESTRUCT_END;
    perl_parse(my_perl, xs_init, argc, argv, (char **)NULL);
    perl_run(my_perl);
    perl_destruct(my_perl);
    perl_free(my_perl);
    PERL_SYS_TERM();
}


이제 이 c소스를 컴파일 해보자.(strawberry perl이면 gcc가 기본으로 포함되어 있고 Activestate Perl이면 ppm install MingGW 명령으로 gcc를 설치할 수 있다.)

gcc -Wall -mwindows -o launch.exe perlxsi.c [이 뒤에는 아래 명령에 의해 나오는 출력결과(컴파일 옵션)들을 각각 긁어 붙인다.

perl -MExtUtils::Embed -e ccopts
perl -MExtUtils::Embed -e ldopts


본인의 경우 컴파일 명령은 다음과 같았다.(실행파일명은 -o 옵션뒤에 원하는 이름으로 하면 됨)

gcc -Wall -mwindows -o launch.exe perlxsi.c -s -O2 -DWIN32 -DHAVE_DES_FCRYPT  -DUSE_SITECUSTOMIZE -DPERL_IMPLICIT_CONTEXT -DPERL_IMPLICIT_SYS -fno-strict-aliasing -mms-bitfields -DPERL_MSVCRT_READFIX  -I"C:\strawberry-perl-5.12.3.0-portable\perl\lib\CORE" -s -L"C:\strawberry-perl-5.12.3.0-portable\perl\lib\CORE" -L"C:\strawberry-perl-5.12.3.0-portable\c\lib"  C:\strawberry-perl-5.12.3.0-portable\perl\lib\CORE\libperl512.a C:\strawberry-perl-5.12.3.0-portable\c\i686-w64-mingw32\lib\libmoldname.a C:\strawberry-perl-5.12.3.0-portable\c\i686-w64-mingw32\lib\libkernel32.a C:\strawberry-perl-5.12.3.0-portable\c\i686-w64-mingw32\lib\libuser32.a C:\strawberry-perl-5.12.3.0-portable\c\i686-w64-mingw32\lib\libgdi32.a C:\strawberry-perl-5.12.3.0-portable\c\i686-w64-mingw32\lib\libwinspool.a C:\strawberry-perl-5.12.3.0-portable\c\i686-w64-mingw32\lib\libcomdlg32.a C:\strawberry-perl-5.12.3.0-portable\c\i686-w64-mingw32\lib\libadvapi32.a C:\strawberry-perl-5.12.3.0-portable\c\i686-w64-mingw32\lib\libshell32.a C:\strawberry-perl-5.12.3.0-portable\c\i686-w64-mingw32\lib\libole32.a C:\strawberry-perl-5.12.3.0-portable\c\i686-w64-mingw32\lib\liboleaut32.a C:\strawberry-perl-5.12.3.0-portable\c\i686-w64-mingw32\lib\libnetapi32.a C:\strawberry-perl-5.12.3.0-portable\c\i686-w64-mingw32\lib\libuuid.a C:\strawberry-perl-5.12.3.0-portable\c\i686-w64-mingw32\lib\libws2_32.a C:\strawberry-perl-5.12.3.0-portable\c\i686-w64-mingw32\lib\libmpr.a C:\strawberry-perl-5.12.3.0-portable\c\i686-w64-mingw32\lib\libwinmm.a C:\strawberry-perl-5.12.3.0-portable\c\i686-w64-mingw32\lib\libversion.a C:\strawberry-perl-5.12.3.0-portable\c\i686-w64-mingw32\lib\libodbc32.a C:\strawberry-perl-5.12.3.0-portable\c\i686-w64-mingw32\lib\libodbccp32.a C:\strawberry-perl-5.12.3.0-portable\c\i686-w64-mingw32\lib\libcomctl32.a

컴파일 결과로 launch.exe 가 생성되면 해당 파일을 dist 디렉토리로 옮긴다. 이제 launch.exe를 실행해 보자.



짠! 잘 실행된다.



만약 launch.exe에 특정 icon을 넣고 싶으면 http://perl-node-interface.blogspot.com/2011/03/deploy-perl-application-on-windows.html 의 설명대로 적절한 리소스 .rc파일을 만들어서 windres 명령으로 오브젝트 파일로 만든다음 컴파일시 추가하여 해도 되고 그것이 귀찮으면 원하는ico파일을 Win32::Exe 모듈을 깐다음 exe_update(  https://metacpan.org/module/exe_update.pl )을 써서

exe_update --icon=my.ico launch.exe

명령을 통해 쉽게 추가할 수 있다.

이제 모든게 끝났다. 이제 배포는 dist 디렉토리이름을 적절하게 바꾼다음 zip으로 묶어서 배포하고 사용자는 그냥 zip파일을 풀고 .exe파일만 실행시키면 그만이다.

<작업이 끝난 디렉토리 내부 모양>


추가 참고사항:

* dist디렉토리안의 perl.exe는 위에서 batch 파일을 통한 실행시는 필요하지만 별도로 만든 .exe 실행파일형태의 launcher를 통해 실행시키면 직접 perl dll을 로딩하여 사용하기 때문에 지워도 상관없다.
* 위 절차로 만들어진 .exe파일은 기본적으로 명령 콘솔을 사용하지 않는다. 만약 콘솔을 보이게 하려면 위에서 말한 exe_update 명령으로
  exe_update -c launch.exe 로 콘솔창이 보이게 exe_update -g launch.exe 로 콘솔창이 안보이게 바꿀 수 있다.
* 다 쓰고 생각해보니 launch.exe는 메인스크립트 이름을 main.pl 정도로 통일하면 아이콘이랑 실행파일 이름은 임의로 바꿔서 계속 재사용 할 수 있을듯.

댓글 3개:

  1. @crowdy Nttrace가 이 아이디어가 실현되도록 해준 공이 크죠 :)

    답글삭제
  2. see also: http://advent.perl.kr/2012/2012-12-18.html

    답글삭제