티스토리 뷰

패키징 도구

  1. pyinstaller
  2. py2exe
  3. py2app

py2exe는 Windows 전용이며, py2app은 mac 전용이다. 클로스 플랫폼은 아니지만 pyinstaller는 두 운영체제에서 모두 사용할 수 있다. pyinstaller는 맥에서 작성된 코드를 맥용으로만, 윈도우에서 작성된 코드를 윈도우용으로만 빌드할 수 있다. 여기에서는 pyinstaller로 ui 파일을 사용한 gui 앱을 빌드하는 과정에서 겪었던 시행착오에 대한 기록이다.

pyinstaller

문법을 떼고 자신만의 앱을 작성하기 시작하면 드디어 완성된 앱을 패포하고 싶어진다. 지금까지 확인한 바에 의하면 대중적이며 안정적인 배포 유틸리티는 pyinstaller인 듯하다. 터미널용 앱이나 ui를 하드 코딩으로 만든 앱의 경우는 사용이 간단하고 편리했다. 하지만 qt designer로 생성한 ui 파일을 직접 사용한 경우에는 독립 실행형 앱을 제대로 생성하지 못하는 문제가 발생했다.

상황을 정리하면 아래와 같다.

  1. python, PyQt5를 이용하여 GUI 앱을 개발하였다.
    • 코드와 ui를 분리하여 ui는 qt designer를 사용하여 만들었다.
  2. 디자인 파일을 .py 파일로 변환하지 않고 .ui 파일을 그대로 호출하여 사용하는 방식을 사용했다.
    • 이는 ui의 수정이 용이하기 때문이다.
  3. pyinstaller를 이용하는 경우 ui 파일을 찾지 못해 에러가 발생한다.

구글링을 통해 확인한 해결 방법은 아래와 같다. 이 방법은 아마도 stackoverrun의 이 페이지를 참고한 것으로 보인다.

  1. 아래 코드를 .py 파일의 최상단에 추가한다.
import sys
import os

def resource_path(relative_path):
    """ Get absolute path to resource, works for dev and for PyInstaller """
    base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
    return os.path.join(base_path, relative_path)
  1. 원래의 .py 파일에서 .ui 파일을 아래와 같은 방식으로 불러온다. resource_path 함수 안에 .ui파일을 적어주고 반환값을 uic.loadUiType에다가 넘겨주면 된다.
form = resource_path('TicketingMacro.ui')

form_class = uic.loadUiType(form)[0]
  1. pyinstaller에게 .ui 파일을 포함하도록 알려줘야 하는데, 이때 spec 파일을 이용한다. spec 파일은 pyinstaller가 프로그램을 빌드할 때 다양한 옵션을 지정하는 파일이다. 먼저 .py 스크립트만 빌드를 하면 spec 파일이 자동으로 되므로 해당 spec 파일을 수정한 다음 이 파일로 다시 빌드하면 된다.
a = Analysis(
    datas=[ ('TicketingMacro.ui', '.') ],
    ...
    )

하지만 이 방법도 제대로 동작하지 않았다.

내 앱의 경우에는 메인창과 경우에 따라 서브창이 사용되므로 ui 파일이 두 개여서 각각의 윈도우를 호출하는 방식을 2와 같이 수정하고 제대로 동작하는지를 확인하였다. 다음으로 3에 따라 두개의 ui 파일명을 spec파일에 datas=[ ('Notapad.ui', '.'), ('find.ui'. '.') ],와 같이 지정하였다. 이 spec 파일로 앱을 생성하는 과정에서 에러가 나고 말았다.

spec파일을 아래와 같이 수정한 후에는 빌드 자체는 정상적으로 종료되었다. datas는 하나의 인자만을 받기 때문에 복수의 파일을 지정하기 위해서는 튜플을 인자로 갖는 리스트(아래 코드에서는 added_files)를 선언하여 아래와 같이 처리해야 한다.

block_cipher = None
added_files = [ ('Notepad.ui', '.'),
                ('find.ui', '.') ]

a = Analysis(['Notepad.py'],
             pathex=['/Users/dexion/PycharmProjects/ExGui/Notepad'],
             binaries=[],
             datas=added_files,
             hiddenimports=[],
...

그러나 빌드된 프로그램을 실행하면 아래와 같은 에러 메시지를 뱉고 앱이 바로 종료되고 말았다.

objc[94404]: Class RunLoopModeTracker is implemented in both /private/var/folders/jb/3j_05dhn161dxk6_nxjtt14w0000gn/T/_MEIUngtpE/libQt5Core.5.dylib (0x105a81a80) and /Users/dexion/opt/anaconda3/lib/libQt5Core.5.9.7.dylib (0x10c38ca80). One of the two will be used. Which one is undefined.
objc[94404]: Class NotificationReceiver is implemented in both /private/var/folders/jb/3j_05dhn161dxk6_nxjtt14w0000gn/T/_MEIUngtpE/libQt5Widgets.5.dylib (0x10520f1b8) and /Users/dexion/opt/anaconda3/lib/libQt5Widgets.5.9.7.dylib (0x10bb1a1b8). One of the two will be used. Which one is undefined.
objc[94404]: Class QCocoaPageLayoutDelegate is implemented in both /private/var/folders/jb/3j_05dhn161dxk6_nxjtt14w0000gn/T/_MEIUngtpE/libQt5PrintSupport.5.dylib (0x10b4d6ef0) and /Users/dexion/opt/anaconda3/lib/libQt5PrintSupport.5.9.7.dylib (0x10b726ef0). One of the two will be used. Which one is undefined.
objc[94404]: Class QCocoaPrintPanelDelegate is implemented in both /private/var/folders/jb/3j_05dhn161dxk6_nxjtt14w0000gn/T/_MEIUngtpE/libQt5PrintSupport.5.dylib (0x10b4d6f40) and /Users/dexion/opt/anaconda3/lib/libQt5PrintSupport.5.9.7.dylib (0x10b726f40). One of the two will be used. Which one is undefined.
QObject::moveToThread: Current thread (0x7f919e551920) is not the object's thread (0x7f919ffef930).
Cannot move to target thread (0x7f919e551920)

You might be loading two sets of Qt binaries into the same process. Check that all plugins are compiled against the right Qt binaries. Export DYLD_PRINT_LIBRARIES=1 and check that only one set of binaries are being loaded.
This application failed to start because it could not find or load the Qt platform plugin "cocoa"
in "".

Available platform plugins are: cocoa, minimal, offscreen.

Reinstalling the application may fix this problem.
[1]    94403 abort      ./Notepad

단순히 빌드 작업이 정상적으로 종료되었다는 것이 문제의 해결을 의미하는 것은 아니었다. 우선 pyinstaller Available platform plugins are: cocoa, minimal, offscreen.라는 메시지를 구글링을 해보니 아래와 같은 해결책이 제시되어 있었다. 참고

pyinstaller 3.3 및 pyqt 5.6.0을 사용하는 conda python3.6.3과 같은 문제가 발생했습니다. /var/folders/...의 .dylib 파일은 conda 설치 폴더의 파일과 충돌합니다. 그들은 삭제 한 후에도 계속 나타났다.
pyinstaller에게 --runtime-tmpdir에 임시 디렉토리를 지정하여이 작업을 마침내 얻었습니다.
예컨대 pyinstaller --onefile --windowed --runtime-tmpdir /your/temp/directory --exclude-module matplotlib test.py

pyinstaller의 옵션을 통해 간단히 해결할 수 있다는 말에 바로 적용해 보았지만 상황은 바뀌지 않았다. 무엇이 문제일까? 고민하다가 spec 파일을 만드는 과정에서 발생했던 에러 메시지에 주목했다.

ExUser@MBP15  ~/PycharmProjects/ExGui/Notepad  pyi-makespec --noconsole --onefile Notepad.py
zsh: /usr/local/bin/pyi-makespec: bad interpreter: /usr/local/opt/python/bin/python3.7: no such file or directory

상황을 정확히 알 수는 없으나 버전 혹은 path상의 문제가 발생한 것으로 보인다. 모두 사용하고 있는 anaconda의 가상 환경에서 비롯된 것은 아닐까 싶지만 확인할 능력이 부재하다. 현재 내가 시도해 볼 수 있는 방법은 관련 패키지의 재설치뿐이다. 우선 가상환경에서 빠져나온 후(conda deactivate) 가장 최근에 설치한 패키지인 pyinstaller를 재설치하였다.

ExUser@MBP15  ~/PycharmProjects/ExGui/Notepad  pip3 install pyinstaller
Processing /Users/ExUser/Library/Caches/pip/wheels/57/9a/e0/213da356866201eac897534a77c7af30b48b48c2734e30a25f/PyInstaller-3.6-py3-none-any.whl
Requirement already satisfied: setuptools in /usr/local/lib/python3.8/site-packages (from pyinstaller) (49.2.0)
Collecting macholib>=1.8
  Using cached macholib-1.14-py2.py3-none-any.whl (37 kB)
Collecting altgraph
  Using cached altgraph-0.17-py2.py3-none-any.whl (21 kB)
Installing collected packages: altgraph, macholib, pyinstaller
Successfully installed altgraph-0.17 macholib-1.14 pyinstaller-3.6

이후 pyi-makespec를 이용하여 spec 파일을 만들었더니 오류 없이 파일이 생성되었다.

ExUser@MBP15  ~/PycharmProjects/ExGui/Notepad  pyi-makespec --noconsole --onefile Notepad.py
wrote /Users/ExUser/PycharmProjects/ExGui/Notepad/Notepad.spec
now run pyinstaller.py to build the executable

생성된 spec 파일에 위와 같이 ui 파일 관련 내용(added\_files = [ ('Notepad.ui', '.’), ('find.ui', '.') ] … datas=added\_files, …)을 추가하였다. 이 spec 파일로 빌드를 했더니, 와우, 앱이 정상적으로 빌드되고 정상적으로 실행되었다. 드디어 해결했다!!

ExUser@MBP15  ~/PycharmProjects/ExGui/Notepad  pyinstaller --clean --runtime-tmpdir ~/PycharmProjects/ExGui/Notepad/temp --exclude-module matplotlib Notepad.spec

해결 방안

결국 근 이틀에 가까운 시간을 허비하게 만들었던 문제의 해결책은 아래와 같다.

  1. 먼저 자신의 개발 환경에서 최신의 pyinstaller를 (재)설치한다.
  2. pyi-makespec --noconsole --onefile test.py으로 .spec 파일을 만든다.
  3. 생성된 test.spec 파일에 추가해야 할 ui 파일을 정확히 지정해 준다. 이때 복수의 ui 파일을 지정해야 한다면 added_files와 같은 리스트를 지정해야 한다는 점에 주의하자. datas가 하나의 인자만을 받기 때문이다.
block_cipher = None
added_files = [ ('userFile1.ui', '.'),
                ('userFile2.ui', '.') ]

a = Analysis(['Notepad.py'],
             pathex=['/Users/dexion/PycharmProjects/ExGui/Notepad'],
             binaries=[],
             datas=added_files,
             hiddenimports=[],
...
  1. 편집된 spec 파일로 빌드를 한다(pyinstaller --clean --runtime-tmpdir ~/PycharmProjects/ExGui/Notepad/temp --exclude-module matplotlib test.spec)

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함
04-29 06:14