이번 추석 연휴에는 그동안 미루고 미뤘던 회사 MacBook을 정리하기로 했다. 입사하고 3년 동안 한 번도 포맷하지 않은 노트북이라 회사 일을 하면서 설치한 개발도구와 테스트 프로그램이 계속 쌓여 있었고, Python도 이것저것 설치하다 보니 어느 순간부터는 현재 환경이 정상적인지조차 판단하기 어려운 상태가 됐다.
마침 최근 회사에서 진행한 작업도 블로그에 하나씩 정리하고 있었고, 연휴 동안 그 기록을 먼저 마무리한 뒤 노트북을 초기화하기로 했다. 문제는 초기화 자체가 아니라 그다음이었다. 포맷 전까지 잘 빌드되던 기존 ESP32 프로젝트를 같은 ESP-IDF 4.4.3 환경으로 다시 구성했는데, 최신 macOS와 Python 환경에서는 예전에 보지 못했던 오류가 연달아 나타났다.
3년 만의 개발환경 초기화
새 환경에는 Homebrew를 다시 설치하고 Python도 최신 버전으로 구성했다. 당시 python3는 Python 3.14.7을 가리키고 있었다. 기존 ESP32 프로젝트를 받아 make를 실행하자 프로젝트의 bootstrap 과정에서 ESP-IDF Python 환경이 없는 것을 확인하고 자동으로 install.sh esp32를 실행했다.
ESP-IDF Python venv not found at /Users/gon/.espressif/python_env/ — running install.sh esp32 (one-time setup)...
cd "/Users/gon/proj/ESP32/plus/external/esp-idf" && ./install.sh esp32
Detecting the Python interpreter
Checking "python" ...
.../tools/detect_python.sh: line 16: python: command not found
Checking "python3" ...
Python 3.14.7
"python3" has been detected
toolchain과 OpenOCD까지는 정상적으로 내려받았다. 처음 문제가 발생한 곳은 Python 환경을 만드는 단계였다.
Installing Python environment and packages
pip 26.2.1 from /opt/homebrew/lib/python3.14/site-packages/pip (python 3.14)
Installing virtualenv
error: externally-managed-environment
× This environment is externally managed
...
hint: See PEP 668 for the detailed specification.
ESP-IDF 4.4.x의 설치 스크립트는 virtualenv가 없으면 pip install --user virtualenv 방식으로 설치를 시도한다. 하지만 새로 설치한 Homebrew Python에는 PEP 668 정책이 적용돼 있었고, 이 방식의 사용자 영역 패키지 설치가 차단됐다. 결국 virtualenv 설치에 실패하면서 ESP-IDF Python 환경도 만들지 못했다.
ModuleNotFoundError: No module named 'virtualenv'
subprocess.CalledProcessError:
Command '['/opt/homebrew/opt/python@3.14/bin/python3.14',
'-m', 'pip', 'install', '--user', 'virtualenv']'
returned non-zero exit status 1.
make: *** [idf-bootstrap] Error 1
처음 로그만 보면 Python 3.14 자체가 ESP-IDF 4.4.3과 맞지 않는 것처럼 보일 수 있다. 하지만 이 시점에서 확인된 직접적인 문제는 Python 언어 버전 자체보다 구형 ESP-IDF의 설치 방식과 최신 Homebrew Python의 패키지 관리 정책이 충돌한 것이었다.
시스템 Python 분리
그렇다고 Python 전체를 예전 버전으로 되돌리고 싶지는 않았다. 회사에서 사용하는 다른 Python 프로젝트, 특히 MySerial 작업은 최신 Python 환경을 기준으로 계속 진행하고 있었기 때문이다. Python은 버전에 따라 설치 가능한 패키지와 의존성 조합이 달라질 수 있어서 ESP-IDF 하나 때문에 python3 자체를 3.9 계열로 바꾸면 다른 프로젝트에 영향을 줄 수 있었다.
ChatGPT와 원인을 정리하면서 우선 python3는 Homebrew의 Python 3.14.x를 그대로 유지하고, ESP-IDF의 detect_python.sh가 먼저 확인하는 python 명령만 macOS에 기본으로 포함된 Python 3.9.6을 가리키도록 우회했다.
sudo tee /usr/local/bin/python >/dev/null <<'EOF'
#!/bin/sh
exec /usr/bin/python3 "$@"
EOF
이 상태에서는 ESP-IDF 설치가 정상적으로 진행됐고, ~/.espressif/python_env/idf4.4_py3.9_env 형태의 Python 3.9 가상환경도 만들어졌다. 당시에는 일단 첫 번째 문제를 넘긴 셈이었다. 다만 이 방법은 최종 구조로 유지하려던 해결책은 아니었다. 시스템 PATH에 전역 python 명령을 하나 추가한 임시 우회였고, 실제 빌드 환경까지 안정적으로 분리하려면 프로젝트 쪽에서 사용하는 Python을 명확하게 선택할 필요가 있었다.
설치 이후의 빌드 실패
ESP-IDF 설치가 끝난 뒤 실제 프로젝트를 빌드하자 이번에는 CMake configure 단계로 넘어가기 전에 다시 멈췄다.
/Library/Developer/CommandLineTools/usr/bin/make -f arch/esp32/esp32.mk
preparing build/esp-idf
source "/Users/gon/proj/ESP32/plus/external/esp-idf/export.sh" >/dev/null && cmake arch/esp32/entry \
-Bbuild \
-DCMAKE_POLICY_VERSION_MINIMUM=3.10 \
-DCMAKE_TOOLCHAIN_FILE=/Users/gon/proj/ESP32/plus/external/esp-idf/tools/cmake/toolchain-esp32.cmake \
-DCMAKE_BUILD_TYPE=MinSizeRel \
-DBASEDIR=/Users/gon/proj/ESP32/plus \
-GNinja
fatal: No names found, cannot describe anything.
WARNING: Git describe was unsuccessful: b''
make[1]: *** [build/esp-idf] Error 1
make: *** [all] Error 2
화면에서 가장 눈에 띄는 것은 git describe 경고였다. 처음에는 나도 이 메시지를 원인으로 의심했고, ChatGPT와 로그를 다시 보면서 fresh clone 이후 submodule 상태와 Git tag, 최신 CMake와 오래된 ESP-IDF 사이의 호환 가능성까지 하나씩 확인했다. 하지만 나중에 확인한 결과 이 메시지는 실제 빌드를 중단시킨 직접 원인이 아니었다. ESP-IDF submodule을 별도로 수정할 필요도 없었고, fresh clone 후 git submodule update --init --recursive로 필요한 nested submodule까지 정상적으로 받아졌다.
setuptools 82와 pkg_resources 제거
이후 Codex에게 실제 저장소와 Python 환경을 확인하도록 했다. 여기서 ESP-IDF 4.4.x의 requirements.txt와 tools/check_python_dependencies.py를 직접 대조하면서 원인이 Python 가상환경 안쪽에 있다는 것이 확인됐다. ESP-IDF 4.4.x의 requirements.txt는 setuptools에 대해 setuptools>=21만 지정하고 있었고 상한 버전은 고정하지 않았다. 반면 의존성 검사 코드에서는 pkg_resources를 직접 import하고 있었다.
Codex가 당시 실패 로그와 가상환경을 확인한 결과, 포맷 후 새로 만들어진 환경에는 setuptools 82.0.1이 설치돼 있었고 그 상태에서 pkg_resources import가 실패하고 있었다. 이후 변경 이력까지 다시 확인해 보니 setuptools 82.0.0부터 pkg_resources가 제거됐다.
ESP-IDF 4.4.x
└─ check_python_dependencies.py
└─ import pkg_resources
setuptools 82.0.1
└─ pkg_resources 없음
↓
ESP-IDF dependency check 실패
여기서 중요한 점은 80.9.0을 공식적인 최대 호환 버전으로 확인해서 선택한 것은 아니라는 것이다. Codex가 setuptools 80.9.0으로 낮춘 환경에서 다시 빌드를 수행했고, make clean && make가 끝까지 성공하는 것을 확인했다. 이후 왜 하필 80.9.0이었는지를 다시 확인해 보니 이 버전이 공식적인 마지막 호환 버전으로 선정된 것은 아니었다.
따라서 80.9.0은 이 프로젝트에서 정상 빌드를 확인한 버전이라고 보는 것이 정확하다. 80.10.x나 81.0.0 같은 다른 버전까지 모두 시험해서 최대 호환 범위를 찾은 것은 아니다.
포맷 전 환경과 새 가상환경
이 지점에서 포맷 전에는 같은 소스와 같은 ESP-IDF 4.4.x로 왜 계속 빌드가 됐는지도 설명이 됐다. 포맷 전에는 오래전에 만들어 둔 ~/.espressif/python_env를 계속 재사용하고 있었다. 그 안에는 당시 정상 동작하던 Python과 setuptools 조합이 이미 들어 있었기 때문에 최신 패키지 생태계의 변화가 빌드 환경에 다시 반영될 일이 없었다. 반대로 포맷 후에는 그 가상환경을 처음부터 다시 만들면서 2026년 시점의 최신 패키지가 설치됐다. SDK 소스는 그대로였지만 Python 가상환경의 내용은 더 이상 예전과 같지 않았다.
현재 환경과 백업해 둔 .espressif를 직접 비교해 보니 같은 idf4.4_py3.9_env라는 이름을 사용하면서도 site-packages의 크기부터 달랐다. 가상환경 이름이 같다고 해서 그 안의 setuptools나 다른 간접 의존성까지 동일하다는 의미는 아니었다.
ESP-IDF venv 우선 적용
setuptools 문제를 해결한 뒤에도 한 가지가 더 남았다. ESP-IDF용 Python 3.9 가상환경을 만들어 두었는데 실제 빌드 과정에서는 여전히 Homebrew의 Python 3.14.7이 영향을 주고 있었다. 여기서는 Codex에게 시스템 Python은 그대로 유지하면서 ESP32 빌드에서만 ESP-IDF 전용 가상환경을 사용하도록 수정하는 방향으로 작업을 맡겼다.
그 결과 arch/esp32/esp32.mk에서 ESP-IDF venv의 bin 디렉터리를 PATH 앞쪽에 배치하도록 변경했다.
-IDF_ENV := source "$(IDF_PATH)/export.sh" >/dev/null &&
+IDF_VENV_PYTHON := $(firstword $(wildcard $(IDF_TOOLS_PATH)/python_env/*/bin/python))
+IDF_ENV := export PATH="$(dir $(IDF_VENV_PYTHON)):$$PATH" && source "$(IDF_PATH)/export.sh" >/dev/null &&
bootstrap 과정에서 선택된 ESP-IDF venv의 bin 디렉터리를 PATH 앞쪽에 넣은 뒤 export.sh를 실행하도록 한 것이다. 이렇게 하면 일반 개발환경에서는 Homebrew Python 3.14.x를 그대로 사용할 수 있고, ESP32 빌드에서는 ESP-IDF 4.4용 Python 3.9 환경을 우선 사용할 수 있다.
일반 Python 개발
└─ Homebrew Python 3.14.x
└─ MySerial 등
ESP32 build
└─ arch/esp32/esp32.mk
└─ ~/.espressif/python_env/idf4.4_py3.9_env/bin
└─ Python 3.9
└─ ESP-IDF 4.4.3
이 방식으로 프로젝트 빌드와 시스템 Python 환경의 영향을 분리할 수 있었고, 앞에서 임시로 만들었던 /usr/local/bin/python 우회에 의존하지 않는 방향으로 정리할 수 있었다.
CMake 4.4.3 확인
최신 개발환경을 다시 구성한 만큼 CMake 버전 역시 처음에는 의심 대상이었다. 현재 설치된 버전은 CMake 4.4.3이었고, ESP-IDF 4.4.3은 몇 년 전 SDK라 버전 차이가 상당했다. 하지만 프로젝트의 arch/esp32/esp32.mk에는 이미 다음 compatibility 옵션이 적용돼 있었다.
-DCMAKE_POLICY_VERSION_MINIMUM=3.10
CMake를 낮추지 않고 4.4.3을 그대로 유지한 상태에서 최종 clean build가 성공했기 때문에, 적어도 이 프로젝트에서 직접적인 빌드 실패 원인은 CMake 버전이 아니었다. 같은 수정 과정에서 Codex는 Make dependency도 함께 정리했다. 기존 build/esp-idf 디렉터리 대신 Ninja configure가 정상적으로 끝났을 때 생성되는 build.ninja를 기준으로 변경했다.
-all $(TARGET): $(BUILDIR)/esp-idf
+all $(TARGET): $(BUILDIR)/build.ninja
-$(BUILDIR)/esp-idf:
+$(BUILDIR)/build.ninja:
이 변경은 Python 버전 문제를 해결하기 위한 것은 아니고, CMake configure가 정상적으로 끝났는지를 더 명확한 산출물로 판단하기 위한 Make dependency 정리였다.
최종 빌드 검증
최종적으로는 ESP-IDF submodule을 수정하지 않고 CMake 4.4.3도 그대로 유지했다. ESP-IDF Python 환경에서는 이 프로젝트에서 검증한 setuptools 80.9.0을 사용하고, Makefile에서는 ESP-IDF 전용 venv를 명시적으로 우선하도록 정리했다.
Codex가 수정된 환경에서 make clean && make를 다시 수행했고, configure부터 전체 빌드가 끝까지 진행되는 것을 확인했다. 나는 그 결과를 바탕으로 실제 프로젝트 환경에 적용했고, ELF와 build/esp32.bin 생성까지 확인했다. 처음에는 최신 macOS에서 오래된 ESP-IDF가 더 이상 빌드되지 않는 문제처럼 보였다. 실제로는 PEP 668, 새로 설치된 setuptools, pkg_resources 제거, 그리고 빌드 시 선택되는 Python 경로가 서로 이어져 있었다.
포맷 전까지 같은 프로젝트가 계속 빌드됐던 이유도 결국 여기에 있었다. SDK 버전은 고정돼 있었지만 그동안 사용하던 Python 가상환경까지 새로 만들지는 않았기 때문이다. 3년 만에 개발환경을 깨끗하게 다시 만들고 나서야, SDK 버전만 고정하는 것으로는 몇 년 뒤의 clean install까지 같은 환경을 보장할 수 없다는 것을 실제 빌드 과정에서 확인하게 됐다. 이번 문제는 로그만 보고 한 번에 원인을 찾은 것이 아니라, ChatGPT와 가능성을 좁히고 Codex로 실제 저장소와 환경을 확인하면서 하나씩 배제해 나간 과정에 가까웠다.
ChatGPT와 Codex가 없으면 이제 개발하기 싫어질 것 같다. 이걸 내가 직접 원인 찾아서 분석하고 수정하려면 하루 종일 삽질했을 텐데, 지금은 불과 20~30분 만에 해결되는 시대가 왔다. 내가 굳이 이걸 왜 혼자 다 하고 있어야 하나 싶다.