본문 바로가기
공부/Python

[python] pyinstaller 일곱번의 삽질 for uvicorn + FastAPI

by 병진들 2021. 6. 18.

pyinstaller 란.. 

python 파일을 패키징해서 executable 형식으로 만들어주는 아주 좋은 도구이다.

사용방법이 굉장히 간단해 보이지만, 실제로 적용하고 세부 설정을 하다보니 너무 할게 많았고 디버깅이 잘 되지 않아 삽질을 많이 했다. 심지어 포스팅 된 글들도 상대적으로 적어서..

 

일단 필수적으로 알고 가야할게 있다.

window / Linux 둘다 pyinstaller를 사용할 수 있다!

다만 당연한 이야기일 수 있지만, window에서 빌드한것은 linux 에서 사용할 수 없고,  linux에서 빌드한것은 windows 에서 사용할 수 없다. (java gradle로 war파일을 만들면 window, linux 상관없이 동작하는거에 비해 불편하긴 하다.)

 

설치

pip install pyinstaller

 

내 환경

더보기

OS: windows 10 / Linux Ubuntu 20.04

Python Version : 3.8x (python venv 사용)

빌드대상파일: main.py

목적: FastAPI Server 를 exe파일로 만들어 배포하기

 

사용방법

Pyinstaller doc: https://pyinstaller.readthedocs.io/en/stable/

pyinstaller [option] [filename.py]

# file name: main.py 인 경우
pyinstaller main.py

 

사실 사용 방법은 굉장히 간단하다.

하지만 정말 제대로 사용하려면 빌드 후 생성되는 main.spec 파일을 잘 뜯어 고쳐야 한다. 

잘 고친 후에는 

pyinstaller main.spec

이렇게 옵션이 적용 된 spec 파일로 빌드를 해줘야 한다.

 

이제부터 빌드하면서 생긴 오류.. 삽질을 공유하겠다.

 

삽질1

main.spec 파일로 계속 빌드를 했지만, 옵션이 적용되지 않는 문제

 

이 경우는 내가 멍청해서 일어난 일이다. 

예를들어

pyinstaller -D 옵션은 빌드한 파일을 하나의 Directory 안에 넣고 그 안에 여러개의 필요한 파일들이 들어있게된다.

pyinstaller -F 옵션은 하나의 exe파일만 떨어지므로 굉장히 간편하게 보이지만 어떤 특정 파일을 참조하는 코드가 있다면 골치아파진다.

 

그런데 이 옵션은 main.py 에 우선적으로 적용되고, main.spec은 옵션을 무시한다.

(정확하게 말하면 spec안에 있는 값만 참조함)

 

# pyinstaller -F main.py 

를 사용해서 빌드한 exe 파일이 존재하고 이에 맞는 main.spec이 생성되어 spec파일을 수정한 뒤

# pyinstaller -D main.spec

을 아무리 해봐야, -D 옵션은 적용되지도 않고 무시된다. 

따라서 다시 -D옵션으로 생성하고 싶다면

# pyinstaller -D main.py 를 다시 써주어 적용해야 한다.

 

당연한 이야기 일 수 있지만, .... 오류하나 안나기 때문에 엄청 답답했다... (pyinstaller 지우고 다시깔아보기도했음 ㅠ)

 

삽질2

Python lib가 제대로 import 되지 않는 문제 (no module name 'lib')

 

분명 pip freeze 로 lib확인도 해보고 pyinstaller 도 다시 해보고.. python main.py 로 실행하면 잘 되고...

왜 module을 찾을 수 없다는걸까 했는데, 

pyinstaller는 특정 모듈을 import하지 못하는경우가 있다. 이 에러는 흔한 문제인지 검색하니 바로 나왔다.

 

내 경우에 uvicorn 모듈을 인식하지 못해서 문제가 생겼었는데,

해결방법은 main.spec 파일 내부에 hiddenimports =[] 이 공간에 uvicorn 관련 모듈을 다 박아주면 된다! (필요한거만 박으면됨..)

hiddenimports 옵션에 module 박아버리기

 

삽질3

python 코드 내에서 특정 data를 참조해야 하는데 찾지 못하는 경우

 

pyinstaller는 불친절하게도 python 코드 내부에서 참조하는 데이터들을 전혀 고려해주지 않는다.

 

이 문제는 다른 사람들을 보면 보통 icon 파일을 넣었는데 적용이 안된다던지,

ui를 위해 이미지를 넣었는데 적용이 안된다던지 그런 경우가 많았다.

 

내 경우엔 File 입출력기능과, 개인적인 dictionary를 참조한다던가,

나중에 포스팅하겠지만 conf.ini 파일을 생성하여 환경변수를 빌드 후에도 변경 할 수 있게 한다던가 했기 때문에 사용하였다.

 

해결방법!

일단 ./dist/main 폴더를 기본 경로로 잡아주는게 중요하다.

# 내 환경 DIRECTORY 구조
# ./dist/main/main.py
# ./dist/main/common/const.py  # 환경설정 변수들이 들어있는 파일

# python source code 내에서 현재 파일의 위치를 가져옴
(os.path.dirname(os.path.abspath(__file__))

# 내 경우. const.py 위치를 기준으로 BASE_DIR를 설정
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # ./dist/main

# 참조할 파일
KEYWORD_DIC = f'{BASE_DIR}/common/dic.txt'

  위 소스코드 처럼 BASE_DIR를 지정해 주고 main.spec 파일에서 datas = [] 부분을 수정해주면 빌드 후에도 파일을 참조할 수 있다.

common/dic.txt 참조하기 위한 main.spec 내부

※ 주의

빌드시 -F 옵션으로 빌드하게 되면 하나의 .exe 파일만 떨어지게 되는데 이 경우에 ubuntu 환경에서는

BASE_DIR가 /tmp/_MEIxxxxxx/ 로 지정되고 사용되기 때문에 파일을 정상적으로 읽어오지 못한다.

그래서 나는 반 강제적으로 일단 -D로 빌드하긴 했는데, 분명 다른 방법이 있을거라 생각한다.. 댓글로 알려주시면..

 

삽질4

Linux 환경에서는 빌드 후 main 실행파일이 생성되는데, 이 파일의 형식은 application/x-executable 이다.

바로 실행이 안되기 때문에 권한 설정이 필요하다.

# main 파일 권한 변경
> sudo chmod 777 ./main

# 실행
> ./main

사실 나중엔 root 아래 파일도 사용하게 되어서 지금은

> sudo ./main

으로 실행한다.... 

 

 

삽질5

UPX is not abailable  (UPX - pyinstaller를 좀 더 빠르게 도와주는 친구)

 

정확하게 어떤 상황에서 이 에러가 갑자기 뜨는지 모르겠지만, 가끔 UPX is not available 이라는 ERROR가 뜰 때가 있다.

경험상 보통 main.spec 파일이 초기화 된 경우에 이런 에러가 나타났는데 main.spec 내부에 upx=True 를 False로 바꿔주어도 되고

 

아래 링크를 참조하여 UPX를 따로 다운로드 후 지정해주어도 된다.

https://www.tutorialexample.com/fix-pyinstaller-upx-is-not-available-error-pyinstaller-tutorial/

 

사실 난 그냥 main.spec을 백업해 두고, 이 에러가 떴을때 덮어쓰기 해버렸다. 제일편하다.

 

삽질6

failed to execute script

 

exe파일을 실행했을 때, 이 에러가 뜨면 또 지긋지긋한 경로문제가 발생한 것이다. 

삽질3을 참조하여 해결할 수 있다..!!

 

 

삽질7

uvicorn 실행 오류.  Error loading ASGI app. Could not import module "main".

 

이 또한 경로문제리...

 

일반적으로 uvicorn을 실행하는 명령어는 아래와 같다.

app = create_app()
if __name__ == "__main__":
	uvicorn.run("main:app", host="0.0.0.0", port=8002, reload=False)

하지만 쉽게 될리가 없지

python main.py로 실행하면 잘 동작하지만, 

pyinstaller로 빌드 후 ./main 하면 Could not import module "main". 같은 오류가 뜬다.

 

main.py를 아래처럼 수정 후 빌드하면 정상 동작한다.

app = create_app()
def serve():
    uvicorn.run(app, host=APP_HOST_ADD, port=APP_PORT)
    
if __name__ == "__main__":
    serve()

 

혹시 또 추가하게 될 삽질이 있다면,, 공유하겠다..

댓글