Python: distlib의 launcher 소스 읽기 - grizlupo/_ GitHub Wiki

distlib은 윈도우즈에서 간단한 실행파일(.exe)로 특정 파이썬 스크립트를 실행시키는 방법을 가지고 있다. setuptools에도 비슷한 기능이 있는데 개인적으로 이 방식이 더 좋아 보인다. 그래서 그 실행파일의 소스를 찾아 읽으면서 설명을 덧붙여 기록해 둔다.

/*
 * Copyright (C) 2011-2015 Vinay Sajip. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#ifndef _WIN32_WINNT            // Specifies that the minimum required platform is Windows Vista.
#define _WIN32_WINNT 0x0600     // Change this to the appropriate value to target other versions of Windows.
#endif

#include <stdio.h>
#include <windows.h>
#include <Shlwapi.h>

#pragma comment (lib, "Shlwapi.lib")

몰랐던 API라 찾아보니, Shell Path Handling Functions 경로를 조작하는 이런저런 API들이다. 우리말로 간단히 번역 정리된 것(http://mindgear.tistory.com/242)도 있어 함께 기록해 둔다.

#define APPENDED_ARCHIVE

매크로가 정의되면 파일의 끝에 스크립트를 포함하는 distlib의 방식을 사용하고 아니면 setuptools처럼 같은 이름의 스크립트를 찾아서 실행하는 방식을 사용한다.

#define MSGSIZE 1024
#if !defined(APPENDED_ARCHIVE)

static wchar_t suffix[] = {
#if defined(_CONSOLE)
    L"-script.py"
#else
    L"-script.pyw"
#endif
};

#endif

setuptools가 같은 이름의 스크립트를 찾는다고는 하지만 완전히 같은 것은 아니고, 스크립트의 이름 끝에는 '-script'가 덧붙는다. 당연히 이 부분은 APPENDED_ARCHIVE 매크로가 정의되지 않은 경우에만 사용된다.

static int pid = 0;

#if !defined(_CONSOLE)

typedef int (__stdcall *MSGBOXWAPI)(IN HWND hWnd, 
        IN LPCSTR lpText, IN LPCSTR lpCaption, 
        IN UINT uType, IN WORD wLanguageId, IN DWORD dwMilliseconds);

#define MB_TIMEDOUT 32000

int MessageBoxTimeoutA(HWND hWnd, LPCSTR lpText, 
    LPCSTR lpCaption, UINT uType, WORD wLanguageId, DWORD dwMilliseconds)
{
    static MSGBOXWAPI MsgBoxTOA = NULL;
    HMODULE hUser = LoadLibraryA("user32.dll");

    if (!MsgBoxTOA) {
        if (hUser)
            MsgBoxTOA = (MSGBOXWAPI)GetProcAddress(hUser, 
                                      "MessageBoxTimeoutA");
        else {
            /*
             * stuff happened, add code to handle it here 
             * (possibly just call MessageBox())
             */
        }
    }

    if (MsgBoxTOA)
        return MsgBoxTOA(hWnd, lpText, lpCaption, uType, wLanguageId,
                         dwMilliseconds);
    if (hUser)
        FreeLibrary(hUser);
    return 0;
}

#endif

gui에서 에러가 발생했을 때 제한시간이 있는 메세지창을 띄운다.

static void
assert(BOOL condition, char * format, ... )
{
    if (!condition) {
        va_list va;
        char message[MSGSIZE];
        int len;

        va_start(va, format);
        len = vsnprintf_s(message, MSGSIZE, MSGSIZE - 1, format, va);
#if defined(_CONSOLE)
        fprintf(stderr, "Fatal error in launcher: %s\n", message);
#else
        MessageBoxTimeoutA(NULL, message, "Fatal Error in Launcher",
                           MB_OK | MB_SETFOREGROUND | MB_ICONERROR,
                           0, 3000);
#endif
        ExitProcess(1);
    }
}

이상이 생겼을 때, console이면 stderr에 gui이면 창을 띄워서 오류 내용을 보이고, 프로그램을 끝낸다.

static wchar_t script_path[MAX_PATH];

#if defined(APPENDED_ARCHIVE)

#define LARGE_BUFSIZE (65 * 1024 * 1024)

typedef struct {
    DWORD sig;
    DWORD unused_disk_nos;
    DWORD unused_numrecs;
    DWORD cdsize;
    DWORD cdoffset;
} ENDCDR;

End of Central Directory는 zip 압축파일의 맨 끝에 있다. zip을 처리하려면 이것부터 찾아야 한다.

/* We don't want to pick up this variable when scanning the executable.
 * So we initialise it statically, but fill in the first byte later.
 */
static char
end_cdr_sig [4] = { 0x00, 0x4B, 0x05, 0x06 };

이것은 zip 압축파일의 서명이다. 덧붙여진 스크립트가 zip 압축되기 때문에 그것을 찾을 때 비교 대상으로서 필요하다. 한데 첫 바이트 0x50('P')이 빠져 있다. 이것은 프로그램이 실행될 때 채워지는데 이렇게 함으로써 실행파일을 뒤질 때 이 부분이 서명으로 오인되는 것을 방지할 수 있다.

static char *
find_pattern(char *buffer, size_t bufsize, char * pattern, size_t patsize)
{
    char * result = NULL;
    char * p;
    char * bp = buffer;
    size_t n;

    while ((n = bufsize - (bp - buffer) - patsize) >= 0) {
        p = (char *) memchr(bp, pattern[0], n);
        if (p == NULL)
            break;
        if (memcmp(pattern, p, patsize) == 0) {
            result = p; /* keep trying - we want the last one */
        }
        bp = p + 1;
    }
    return result;
}

buffer에서 pattern을 찾는다. 여러개라면 마지막 것을 찾는다.

static char *
find_shebang(char * buffer, size_t bufsize)
{
    FILE * fp = NULL;
    errno_t rc;
    char * result = NULL;
    char * p;
    size_t read;
    long pos;
    __int64 file_size;
    __int64 end_cdr_offset = -1;
    ENDCDR end_cdr;

실행파일의 끝에는 shebang과 스크립트가 압축된 zip 압축파일 하나가 줄바꿈 하나를 사이에 두고 나란히 붙어 있다. 줄바꿈은 shebang의 끝을 나타내는 용도다.

    rc = _wfopen_s(&fp, script_path, L"rb");
    assert(rc == 0, "Failed to open executable");
    fseek(fp, 0, SEEK_END);
    file_size = ftell(fp);
    pos = (long) (file_size - bufsize);
    if (pos < 0)
        pos = 0;
    fseek(fp, pos, SEEK_SET);
    read = fread(buffer, sizeof(char), bufsize, fp);

shebang을 찾으려면 먼저 zip 압축파일의 끝에 있는 Central Directory의 끝을 찾아야 한다. 실행파일을 열고 끝에서 버퍼크기만큼을 먼저 읽는다.

    p = find_pattern(buffer, read, end_cdr_sig, sizeof(end_cdr_sig));
    if (p != NULL) {
        end_cdr = *((ENDCDR *) p);
        end_cdr_offset = pos + (p - buffer);
    }

압축파일의 설명 항목이 추가되지 않았다면 end_cdr은 보통 이 단계에서 찾을 수 있다.

    else {
        /*
         * Try a larger buffer. A comment can only be 64K long, so
         * go for the largest size.
         */
        char * big_buffer = malloc(LARGE_BUFSIZE);
        int n = (int) LARGE_BUFSIZE;

        pos = (long) (file_size - n);

        if (pos < 0)
            pos = 0;
        fseek(fp, pos, SEEK_SET);
        read = fread(big_buffer, sizeof(char), n, fp);

압축파일에 설명 항목이 추가되었다면 end_cdr이 좀 더 앞 쪽에 있을 것이고, MAX_PATH 크기로 읽어서는 서명을 못 찾을 수 있다. 이제 버퍼를 키워서 더 많이 읽는다. LARGE_BUFSIZE는 65MB이므로 거의 모든 경우에 실행파일 전체를 읽게 될 것이다.

        p = find_pattern(big_buffer, read, end_cdr_sig, sizeof(end_cdr_sig));
        assert(p != NULL, "Unable to find an appended archive.");
        end_cdr = *((ENDCDR *) p);
        end_cdr_offset = pos + (p - big_buffer);
        free(big_buffer);
    }

더 많이 뒤져도 서명을 찾을 수 없다면 에러로 처리한다.

    end_cdr_offset -= end_cdr.cdsize + end_cdr.cdoffset;
    /*
     * end_cdr_offset should now be pointing to the start of the archive,
     * i.e. just after the shebang. We'll assume the shebang line has no
     * # or ! chars except at the beginning, and fits into bufsize (which
     * should be MAX_PATH).
     */
    pos = (long) (end_cdr_offset - bufsize);
    if (pos < 0)
        pos = 0;
    fseek(fp, pos, SEEK_SET);
    read = fread(buffer, sizeof(char), bufsize, fp);
    assert(read > 0, "Unable to read from file");

cdr을 통해 cdsize와 offset을 구해서 압축파일의 시작위치를 구한다. 여기서 다시 앞쪽으로 bufsize만큼을 읽는다. 찾고자 하는 shebang은 압축파일의 앞쪽에 있다.

    p = &buffer[read - 1];
    while (p >= buffer) {
        if (memcmp(p, "#!", 2) == 0) {
            result = p;
            break;
        }
        --p;
    }
    fclose(fp);
    return result;
}

#endif

뒤에서부터 뒤져서 shebang(#!)을 찾는다.

#if 0
static COMMAND * find_on_path(wchar_t * name)
{
    wchar_t * pathext;
    size_t    varsize;
    wchar_t * context = NULL;
    wchar_t * extension;
    COMMAND * result = NULL;
    DWORD     len;
    errno_t   rc;

    wcscpy_s(path_command.key, MAX_PATH, name);
    if (wcschr(name, L'.') != NULL) {
        /* assume it has an extension. */
        len = SearchPathW(NULL, name, NULL, MSGSIZE, path_command.value, NULL);
        if (len) {
            result = &path_command;
        }
    }
    else {
        /* No extension - search using registered extensions. */
        rc = _wdupenv_s(&pathext, &varsize, L"PATHEXT");
        if (rc == 0) {
            extension = wcstok_s(pathext, L";", &context);
            while (extension) {
                len = SearchPathW(NULL, name, extension, MSGSIZE, path_command.value, NULL);
                if (len) {
                    result = &path_command;
                    break;
                }
                extension = wcstok_s(NULL, L";", &context);
            }
            free(pathext);
        }
    }
    return result;
}

#endif
static wchar_t *
skip_ws(wchar_t *p)
{
    while (*p && iswspace(*p))
        ++p;
    return p;
}

공백을 건너뛴다.

static wchar_t *
skip_me(wchar_t * p)
{
    wchar_t * result;
    wchar_t terminator;

    if (*p != L'\"')
        terminator = L' ';
    else {
        terminator = *p++;
        ++p;
    }
    result = wcschr(p, terminator);
    if (result == NULL) /* perhaps nothing more on the command line */
        result = L"";
    else
        result = skip_ws(++result);
    return result;
}

주어진 문자열에서 다음 공백까지 또는 큰따옴표(")로 묶인 한 덩어리를 건너뛴 위치를 찾는다.

static char *
find_terminator(char *buffer, size_t size)
{
    char c;
    char * result = NULL;
    char * end = buffer + size;
    char * p;

    for (p = buffer; p < end; p++) {
        c = *p;
        if (c == '\r') {
            result = p;
            break;
        }
        if (c == '\n') {
            result = p;
            break;
        }
    }
    return result;
}

주어진 버퍼에서 줄바꿈을 찾는다. CR이든 LF든 먼저 찾은 위치를 돌려준다.

static BOOL
safe_duplicate_handle(HANDLE in, HANDLE * pout)
{
    BOOL ok;
    HANDLE process = GetCurrentProcess();
    DWORD rc;

    *pout = NULL;
    ok = DuplicateHandle(process, in, process, pout, 0, TRUE,
                         DUPLICATE_SAME_ACCESS);
    if (!ok) {
        rc = GetLastError();
        if (rc == ERROR_INVALID_HANDLE)
            ok = TRUE;
    }
    return ok;
}

static BOOL
control_key_handler(DWORD type)
{
    if ((type == CTRL_C_EVENT) && pid)
        GenerateConsoleCtrlEvent(pid, 0);
    return TRUE;
}
static void
run_child(wchar_t * cmdline)
{
    HANDLE job;
    JOBOBJECT_EXTENDED_LIMIT_INFORMATION info;
    DWORD rc;
    BOOL ok;
    STARTUPINFOW si;
    PROCESS_INFORMATION pi;

    job = CreateJobObject(NULL, NULL);
    ok = QueryInformationJobObject(job, JobObjectExtendedLimitInformation,
                                  &info, sizeof(info), &rc);
    assert(ok && (rc == sizeof(info)), "Job information querying failed");
    info.BasicLimitInformation.LimitFlags |= JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE |
                                             JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK;
    ok = SetInformationJobObject(job, JobObjectExtendedLimitInformation, &info,
                                 sizeof(info));
    assert(ok, "Job information setting failed");

Job과 관련한 처리에 대해서는 아직 파악이 되지 않았다.

JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE : Causes all processes associated with the job to terminate when the last handle to the job is closed. This limit requires use of a JOBOBJECT_EXTENDED_LIMIT_INFORMATION structure. Its BasicLimitInformation member is a JOBOBJECT_BASIC_LIMIT_INFORMATION structure.

JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK : Allows any process associated with the job to create child processes that are not associated with the job. If the job is nested and its immediate job object allows breakaway, the child process breaks away from the immediate job object and from each job in the parent job chain, moving up the hierarchy until it reaches a job that does not permit breakaway. If the immediate job object does not allow breakaway, the child process does not break away even if jobs in its parent job chain allow it. This limit requires use of a JOBOBJECT_EXTENDED_LIMIT_INFORMATION structure. Its BasicLimitInformation member is a JOBOBJECT_BASIC_LIMIT_INFORMATION structure.

    memset(&si, 0, sizeof(si));
    si.cb = sizeof(si);
    ok = safe_duplicate_handle(GetStdHandle(STD_INPUT_HANDLE), &si.hStdInput);
    assert(ok, "stdin duplication failed");
    ok = safe_duplicate_handle(GetStdHandle(STD_OUTPUT_HANDLE), &si.hStdOutput);
    assert(ok, "stdout duplication failed");
    ok = safe_duplicate_handle(GetStdHandle(STD_ERROR_HANDLE), &si.hStdError);
    assert(ok, "stderr duplication failed");
    si.dwFlags = STARTF_USESTDHANDLES;

표준 입/출력을 복사한다.

    SetConsoleCtrlHandler((PHANDLER_ROUTINE) control_key_handler, TRUE);

console의 key handler를 설정한다.

    ok = CreateProcessW(NULL, cmdline, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);
    assert(ok, "Unable to create process using '%s'", cmdline);
    pid = pi.dwProcessId;
    AssignProcessToJobObject(job, pi.hProcess);
    CloseHandle(pi.hThread);
    WaitForSingleObject(pi.hProcess, INFINITE);
    ok = GetExitCodeProcess(pi.hProcess, &rc);
    assert(ok, "Failed to get exit code of process");
    ExitProcess(rc);
}
static wchar_t *
find_exe(wchar_t * line) {
    wchar_t * p;

    while ((p = StrStrIW(line, L".exe")) != NULL) {
        wchar_t c = p[4];

        if ((c == L'\0') || (c == L'"') || iswspace(c))
            break;
        line = &p[4];
    }
    return p;
}

'.exe'를 찾는다. 이어지는 문자는 공백이나 큰따옴표여야 한다. 혹은 문자열의 끝이어야 한다. 이런 제한은 다른 단어 속에 포함된 것은 빼려는 것이다.

static wchar_t *
find_executable_and_args(wchar_t * line, wchar_t ** argp)
{
    wchar_t * p = find_exe(line);

    assert(p != NULL, "Expected to find a command ending in '.exe' in shebang line.");
    p += 4;
    if (*line == L'"') {
        assert(*p == L'"', "Expected terminating double-quote for executable in shebang line.");
        *p++ = L'\0';
        ++line;
    }
    /* p points just past the executable. It must either be a NUL or whitespace. */
    assert(*p != L'"', "Terminating quote without starting quote for executable in shebang line.");

shebang 줄에서 .exe로 끝나는 명령을 찾는다. 전체가 큰따옴표(")로 묶여 있으면 따옴표를 없앤다. 따옴표가 끝에만 있다면 에러다.

    /* if p is whitespace, make it NUL to truncate 'line', and advance */
    if (*p && iswspace(*p))
        *p++ = L'\0';
    /* Now we can skip the whitespace, having checked that it's there. */
    while(*p && iswspace(*p))
        ++p;
    *argp = p;
    return line;
}

첫 하나의 공백은 명령의 끝을 표시하기 위해 NUL로 바꾸고, 나머지 앞쪽의 공백은 모두 없앤다. 그런 다음에도 남은 것은 명령의 인수다.

static int
process(int argc, char * argv[])
{
    wchar_t * cmdline = skip_me(GetCommandLineW());
    wchar_t * psp;
    size_t len = GetModuleFileNameW(NULL, script_path, MAX_PATH);

cmdline에서 첫 항목인 실행 프로그램의 이름은 제외시킨다. 실제로 스크립트를 실행시킬 프로그램(python.exe)의 완전한 이름은 shebang을 통해 얻는다.

cmdline에서 제외시킨 프로그램의 이름은 GetModuleFileNameW 통해서 대신 얻고, 방식에 따라 '-script.py' 따위의 접미사를 추가하거나 이름 그대로를 python.exe가 실행시킬 스크립트 이름으로 사용한다(psp).

애초의 .exe 확장자가 붙은 프로그램은 맨 뒤에 zip 압축파일 하나가 붙어 있다. 그 안에는 'main.py'라는 파일이 하나 포함되어 있다. python.exe로 zip 파일을 실행하는 경우 내부의 'main.py'를 실행하게 된다. 비록 확장자가 .exe이기는 하지만 그래도 zip 파일로 인식한다.

    FILE *fp = NULL;
    char buffer[MAX_PATH];
    wchar_t wbuffer[MAX_PATH];
    char *cp;
    wchar_t * wcp;
    wchar_t * cmdp;
    char * p;
    wchar_t * wp;
    int n;
#if !defined(APPENDED_ARCHIVE)
    errno_t rc;
#endif
    if (script_path[0] != L'\"')
        psp = script_path;
    else {
        psp = &script_path[1];
        len -= 2;
    }
    psp[len] = L'\0';

공백을 포함한 경우 전체가 큰따옴표(")로 감싸지므로 첫 문자를 확인해서 큰따옴표(")이면 벗겨낸다.

#if !defined(APPENDED_ARCHIVE)
    /* Replace the .exe with -script.py(w) */
    wp = wcsstr(psp, L".exe");
    assert(wp != NULL, "Failed to find \".exe\" in executable name");

    len = MAX_PATH - (wp - script_path);
    assert(len > sizeof(suffix), "Failed to append \"%s\" suffix", suffix);
    wcsncpy_s(wp, len, suffix, sizeof(suffix));
#endif

.exe 확장자를 떼고 suffix(console이면 -script.py이고, gui이면 -script.pyw)를 붙인다. 만약 .exe 확장자가 없거나, suffix를 붙일 수 없을 만큼 파일명이 길면 에러다.

#if defined(APPENDED_ARCHIVE)
    /* Initialise signature dynamically so that it doesn't appear in
     * a stock executable.
     */
    end_cdr_sig[0] = 0x50;

    p = find_shebang(buffer, MAX_PATH);
    assert(p != NULL, "Failed to find shebang");
#else
    rc = _wfopen_s(&fp, psp, L"rb");
    assert(rc == 0, "Failed to open script file \"%s\"", psp);
    fread(buffer, sizeof(char), MAX_PATH, fp);
    fclose(fp);
    p = buffer;
#endif

방식에 따라 실행파일에 덧붙여진 스크립트를 찾거나, 같은 이름의 스크립트 파일을 읽어 들인다. 파일을 뒤지기 전에 앞서 비워두었던 zip 압축파일의 서명에 0x50을 채워서 완성하고 있다.

스크립터의 앞에 있는 shebang을 읽는 것이 목적이므로 버퍼크기는 MAX_PATH(260)로 자그마하다.

    cp = find_terminator(p, MAX_PATH);
    assert(cp != NULL, "Expected to find terminator in shebang line");
    *cp = '\0';

buffer가 shebang을 담고 있다고 보고, 줄바꿈을 찾아 0을 기록함으로써 한 줄의 shebang만 남긴다.

    // Decode as UTF-8
    n = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, p, (int) (cp - p), wbuffer, MAX_PATH);
    assert(n != 0, "Expected to decode shebang line using UTF-8");
    wbuffer[n] = L'\0';

유니코드로 바꾼다. shebang은 utf-8로 인코딩된 것으로 본다.

    wcp = wbuffer;
    while (*wcp && iswspace(*wcp))
        ++wcp;
    assert(*wcp == L'#', "Expected to find \'#\' at start of shebang line");
    ++wcp;
    while (*wcp && iswspace(*wcp))
        ++wcp;
    assert(*wcp == L'!', "Expected to find \'!\' following \'#\' in shebang line");
    ++wcp;
    while (*wcp && iswspace(*wcp))
        ++wcp;

shebang의 시작이 #!인지 확인한다. 중간의 공백은 상관없다. 모두 무시한다.

    wp = NULL;
    wcp = find_executable_and_args(wcp, &wp);
    assert(wcp != NULL, "Expected to find executable in shebang line");
    assert(wp != NULL, "Expected to find arguments (even if empty) in shebang line");
     /* 3 spaces + 4 quotes + NUL */
    len = wcslen(wcp) + wcslen(wp) + 8 + wcslen(psp) + wcslen(cmdline);
    cmdp = (wchar_t *) calloc(len, sizeof(wchar_t));
    assert(cmdp != NULL, "Expected to be able to allocate command line memory");
    _snwprintf_s(cmdp, len, len, L"\"%s\" %s \"%s\" %s", wcp, wp, psp, cmdline);
    run_child(cmdp);  /* never actually returns */
    free(cmdp);
    return 0;
}

shebang으로부터 진짜 파이썬 실행파일의 이름을 얻고, 그걸로 명령줄을 다시 만들고, 실행한다.

#if defined(_CONSOLE)

int main(int argc, char* argv[])
{
    return process(argc, argv);
}

#else

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
                     LPSTR lpCmdLine, int nCmdShow)
{
    return process(__argc, __argv);
}
#endif

distlibpip에 포함되어 있으므로 대부분의 파이썬 배포판은 이미 이를 포함하고 있으므로 간단히 pip._vendor.distlib.scripts 모듈을 import 하면 이용할 수 있다.

from pip._vendor.distlib.scripts import ScriptMaker

maker = ScriptMaker(None, '')
maker.make('test = test:main')

이렇게 하면 현재 디렉토리에 test.exe와 test-3.6.exe가 만들어진다. make 메소드에 전달된 문자열 인수(specification)는 setup.py에 작성에도 그대로 사용된다.

만약 test 모듈을 setup.py로 배포하면서 test.exe가 만들고 싶다면 setup.py에 아래의 내용을 포함시킨다.

setup(
    ...
    entry_points={
            'console_scripts': [
                'test = test:main'
            ]
    ...
)

'test = test:main'에서 '=' 앞은 만들 실행파일의 이름이고, ':'으로 분리된 앞/뒤는 각각 모듈과 거기에 포함된 함수의 이름이다. 여기서 얻은 모듈과 함수를 아래의 스크립트에 갈아 넣어서 실행파일에 포함하게 된다.

# -*- coding: utf-8 -*-
if __name__ == '__main__':
    import sys, re

    def _resolve(module, func):
        __import__(module)
        mod = sys.modules[module]
        parts = func.split('.')
        result = getattr(mod, parts.pop(0))
        for p in parts:
            result = getattr(result, p)
        return result

    try:
        sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])

        func = _resolve('%(module)s', '%(func)s')
        rc = func() # None interpreted as 0
    except Exception as e:  # only supporting Python >= 2.6
        sys.stderr.write('%%s\\n' %% e)
        rc = 1
    sys.exit(rc)

여기서 %(module)s%(func)s 부분을 각각 test와 main으로 바꾸면 실제로 포함되는 스크립트가 된다. 이렇게 틀이 되는 스크립트는 script_template를 바꿈으로써 마음대로 할 수 있다.

실제로 pip는 script_template의 내용을 좀 더 깔끔한 형태로 바꾼다.

# -*- coding: utf-8 -*-
import re
import sys

from %(module)s import %(import_name)s

if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
    sys.exit(%(func)s())

이미 있는 코드를 이용하는 것이 아니라 포함된 실행파일만을 이용하고 싶다면 실행파일 뒤에 shebang과 main.py를 포함한 zip 파일을 덧붙이면 된다. sehbang 뒤에는 반드시 줄바꿈이 있어야 한다.

⚠️ **GitHub.com Fallback** ⚠️