uv 설치 후 버전이 안 바뀔 때, pip install uv 경로 문제

pip install uv 후 버전이 안 바뀔 때 해결법

3줄 요약

  • pip install uv가 Successfully installed uv-0.12.13을 찍은 직후 uv --version이 34일 전 버전인 0.12.3을 그대로 출력했습니다.
  • pip는 ~/.local/bin에 새 바이너리를 넣었고 셸은 /usr/local/bin을 먼저 봅니다. 두 위치 사이에 릴리스 10개가 쌓여 있었는데 pip 경고는 0줄이었습니다.
  • 설치 직후 which -a uv와 python3 -c "import uv; print(uv.find_uv_bin())" 두 줄을 찍어 경로가 같은지 확인하십시오.

터미널에 붙어 있던 두 줄입니다.

Successfully installed uv-0.12.13
uv 0.12.3 (aarch64-unknown-linux-gnu)

위는 pip install uv의 마지막 줄이고 아래는 바로 다음에 친 uv --version의 출력인데요. 설치는 분명히 성공했고 실패 메시지도 경고도 없었습니다. 그런데 실행되는 건 한 달 전 버전이었습니다.

처음엔 셸 해시 캐시를 의심했습니다. hash -r을 쳐도 그대로였고, 새 셸을 띄워도 같았습니다. 그다음엔 설치가 실제로는 실패하고 캐시된 메타데이터만 읽은 건 아닌지 봤는데 그것도 아니었고요. 문제는 캐시가 아니라 같은 이름의 파일이 두 곳에 생긴 것이었습니다.

이 상황이 불편한 건 되돌리기 어려워서가 아닙니다. 알아채기 어려워서입니다. 실패했으면 다시 깔면 그만인데, 성공 메시지를 받고 나면 그 뒤로는 그 명령이 최신이라고 믿고 쓰게 됩니다.

두 파일은 애초에 같은 곳에 있지 않았습니다

pip가 성공했다고 말한 대상과 셸이 실행하는 대상이 다른 파일입니다. 설치 로그의 어느 줄도 그 사실을 말해 주지 않습니다.

로그에 있던 한 줄, 없던 한 줄

깨끗하게 지우고 다시 깔아 전체 출력을 받았습니다. Ubuntu 22.04.5, 시스템 파이썬 3.10.12, pip 25.3 환경입니다.

# 완전히 지우고 재설치해 출력 전체를 확인합니다
$ pip uninstall -y uv --break-system-packages
$ pip install uv --break-system-packages
Defaulting to user installation because normal site-packages is not writeable
Collecting uv
  Using cached uv-0.12.13-py3-none-manylinux_2_28_aarch64.whl.metadata (11 kB)
Using cached uv-0.12.13-py3-none-manylinux_2_28_aarch64.whl (19.4 MB)
Installing collected packages: uv
Successfully installed uv-0.12.13

첫 줄이 유일한 단서입니다. 시스템 site-packages에 쓸 수 없어 사용자 영역으로 내려갔다는 뜻이고, 그러면 바이너리도 ~/.local/bin으로 갑니다. 그런데 그 경로가 PATH에 있는지에 대해서는 아무 말이 없습니다.

이 한 줄은 정보 수준(Defaulting to)이지 경고가 아닙니다. 컨테이너나 CI처럼 시스템 파이썬을 그대로 쓰는 환경에서는 거의 항상 뜨기 때문에 눈에 남지도 않습니다. 매번 보이는 줄은 아무것도 말하지 않는 줄과 같습니다.

같은 이름 두 파일

which -a로 보면 셸이 보는 건 하나뿐입니다. 실제로는 두 개가 있었습니다.

경로 버전 크기 누가 넣었나 PATH에 있나
/usr/local/bin/uv 0.12.3 47,339,352 B 이미지 빌드 시점 있음 (2번째)
~/.local/bin/uv 0.12.13 42,893,648 B 방금 친 pip 없음

두 버전의 PyPI 업로드 시각은 1차 소스로 확인했습니다. 0.12.3은 2026-08-07T16:32:18Z(KST 08-08 01:32), 0.12.13은 2026-09-10T19:25:01Z(KST 09-11 04:25)입니다. 34일 간격이고 그사이 0.12.4부터 0.12.12까지 아홉 개가 더 나왔습니다.

upload_time_iso_8601 값을 그대로 옮기면 이 릴리스 밀도가 보입니다. 패치 간격이 며칠 단위라, 이미지에 박힌 버전과 방금 받은 버전의 거리는 시간이 지날수록 빠르게 벌어집니다.

버전 PyPI 업로드 (UTC) 앞 버전과의 간격
0.12.3 2026-08-07T16:32:18Z — (이미지에 박힌 버전)
0.12.6 2026-08-25T19:38:55Z 18일
0.12.9 2026-09-01T21:57:01Z 7일
0.12.12 2026-09-09T16:43:29Z 8일
0.12.13 2026-09-10T19:25:01Z 1일 (설치된 버전)

uv --help의 서브커맨드 목록을 두 바이너리에서 뽑아 diff로 비교하면 차이가 없습니다. 최상위 옵션 개수도 18개로 같습니다. 겉으로 티가 나지 않는다는 것이 이 상황의 성질입니다. 34일치 버그 수정과 해시 검증 변경이 빠진 채로 조용히 돕니다.

릴리스 노트를 보면 0.12.13에는 PEP 658 메타데이터 사이드카를 받을 때 해시를 검증하는 변경이 들어 있습니다. 이런 항목은 평소에 아무 화면에도 나타나지 않습니다. 빠져 있어도 명령은 그대로 성공하고, 그래서 빠진 줄을 모릅니다.

경고가 빠진 자리는 PATH가 아니라 휠 구조였습니다

pip에는 "스크립트가 PATH에 없다"는 경고가 있습니다. 그게 왜 안 떴는지가 이 함정의 핵심입니다.

휠 안에 무엇이 들어 있나

내려받은 휠을 열어 봤습니다. 파일이 열두 개뿐입니다.

# uv 휠의 내부 구조를 직접 확인합니다
import zipfile, glob
z = zipfile.ZipFile(glob.glob("uv-*.whl")[0])
print(len(z.namelist()))                                    # 12
print([n for n in z.namelist() if ".data/" in n])
# ['uv-0.12.13.data/scripts/uv', 'uv-0.12.13.data/scripts/uvx']
print([n for n in z.namelist() if n.endswith("entry_points.txt")])
# []  ← entry_points.txt 가 없습니다

entry_points.txt가 없습니다. uv는 컴파일된 실행 파일을 .data/scripts/에 그대로 담아 배포하고, pip는 그 파일을 scripts 디렉터리로 복사합니다. 휠 19.4MB 중 대부분이 이 바이너리 둘이고, 파이썬 코드는 래퍼 세 파일이 전부입니다.

pip의 경고는 그 경로를 타지 않습니다. pip/_internal/operations/install/wheel.py의 경고 함수는 generated_console_scripts, 즉 pip가 entry point로 생성한 스크립트만 인자로 받습니다. 복사만 된 파일은 애초에 검사 대상이 아닙니다.

함수 안쪽을 보면 PATH 비교 자체는 꼼꼼합니다. 경로를 정규화해 맞추고, 실행 중인 파이썬과 같은 디렉터리면 venv를 활성화하지 않은 경우로 보고 경고를 빼기까지 합니다. 그 정성이 들어간 검사에 uv가 아예 인자로 도달하지 않는 것입니다.

대조군을 하나 깔아 봤습니다

같은 디렉터리에 entry point 방식 패키지를 넣어 비교했습니다. 조건은 동일합니다.

$ pip install --force-reinstall --no-deps tabulate --break-system-packages
  WARNING: The script tabulate is installed in '~/.local/bin' which is not on PATH.
  Consider adding this directory to PATH or, if you prefer to suppress this
  warning, use --no-warn-script-location.
Successfully installed tabulate-0.10.0

같은 위치, 같은 PATH, 같은 pip인데 이쪽은 경고가 뜹니다. 차이는 휠에 [console_scripts] 항목이 있느냐 하나입니다. 속도를 위해 러스트 바이너리를 그대로 싣는 선택이 경고 경로를 같이 끊었습니다.

경고가 뜬 쪽은 오히려 안전합니다. 메시지를 보고 PATH를 손보든 무시하든 사용자가 알고 결정합니다. 문제는 같은 조건에서 아무 말도 듣지 못한 쪽이고, 그쪽이 하필 매일 치는 명령입니다.

버전 해석이 사람 눈에 안 보이는 곳에서 어긋나는 건 범위 기호 하나가 통과시키는 버전 수를 재 봤을 때와 성격이 같습니다. 그때는 기호의 의미가, 이번엔 파일의 위치가 조용했습니다.

python -m uv로 우회해도 같은 파일이 뜹니다

보통 이럴 때 파이썬 모듈로 부르면 그 파이썬이 아는 바이너리가 실행될 거라 기대합니다. 반대였습니다.

find_uv_bin이 보는 순서

uv 파이썬 패키지는 실제 바이너리를 찾아 os.execvpe로 넘기는 얇은 래퍼입니다. 찾는 순서가 _find_uv.py에 다섯 단계로 적혀 있습니다.

순서 탐색 대상 이 환경에서의 값 결과
1 현재 파이썬의 scripts /usr/local/bin 여기서 잡힘 (0.12.3)
2 base prefix의 scripts /usr/local/bin 중복
3 패키지 루트 상위 (--prefix 설치) 해당 없음 —
4 패키지 루트 인접 (--target 설치) 해당 없음 —
5 user scheme scripts ~/.local/bin 도달하지 못함

새로 설치된 파일은 5번에 있는데 탐색은 1번에서 끝납니다. 순서 자체는 합리적입니다. pip install --prefix나 --target으로 깐 경우까지 챙기려고 네 갈래를 먼저 보는 구조인데, 사용자 영역 설치만 유독 맨 뒤에 놓입니다.

그래서 실행 경로를 무엇으로 바꿔도 같은 답이 나옵니다. 아래는 같은 셸에서 연속으로 찍은 것입니다.

호출 방법 실행된 버전
uv --version 0.12.3
python3 -m uv --version 0.12.3
uvx --version 0.12.3
~/.local/bin/uv --version 0.12.13
PATH=~/.local/bin:$PATH uv --version 0.12.13

절대 경로로 부르거나 PATH 순서를 바꾼 두 줄만 새 버전입니다.

ruff도 같은 파일을 씁니다

ruff 휠을 열어 _find_ruff.py를 꺼내 봤더니 함수 이름과 문자열만 다르고 탐색 순서 다섯 단계가 글자 그대로 같았습니다. 주석까지 동일합니다. uv 하나의 버그가 아니라 Astral 도구들이 공유하는 배포 방식의 성질입니다.

같은 방식으로 배포되는 도구를 여러 개 쓰고 있다면 문제도 같은 수만큼 생깁니다. 포매터 버전이 사람마다 다르면 커밋 diff에 포맷 변경이 섞여 들어오는데, 원인을 찾을 때 각자 --version을 찍어 보면 다들 같은 숫자를 말합니다. 실제로 실행되는 파일이 다르다는 건 그 숫자로는 드러나지 않습니다.

self update가 안내하는 곳이 제자리입니다

여기서 한 번 크게 막혔거든요. 도구가 직접 알려 주는 해결책을 따라갔는데 같은 자리로 돌아왔습니다.

안내대로 하면 되돌아옵니다

$ uv self update
error: Self-update is only available for uv binaries installed via the
standalone installation scripts.

If you installed uv with pip, brew, or another package manager, update uv
with `pip install --upgrade`, `brew upgrade`, or similar.

메시지는 정확합니다. 그런데 pip install --upgrade uv를 실행하면 처음 상황이 그대로 재현됩니다. 새 파일은 ~/.local/bin에 갱신되고, uv self update를 친 그 바이너리는 /usr/local/bin에 그대로 남습니다.

안내가 틀린 게 아니라 자기가 어디에 있는지 모르는 상태에서 나온 안내입니다. 셸이 어느 파일을 잡는지는 uv가 알 수 없습니다. 이 메시지를 보고 pip install --upgrade를 두 번 치고 나서야 뭔가 이상하다고 느꼈습니다.

여기에 확인 절차를 하나 더 얹는다면, 업그레이드 직후 --version이 아니라 which -a를 먼저 보는 습관이 낫습니다. 버전 숫자는 어느 파일이 대답했는지 말해 주지 않습니다.

uvx로 서버를 띄우는 설정에서 더 조용합니다

MCP 서버 설정은 보통 명령을 uvx라고만 적습니다. 어느 uvx가 실행될지는 그 프로세스가 물려받은 PATH가 정합니다.

에디터에서 띄운 프로세스와 터미널의 PATH가 다르면 같은 설정이 서로 다른 바이너리를 부릅니다. 서버가 뜨지 않을 때 stdio 서버의 진짜 오류가 stderr에만 남는 구조와 겹치면, 클라이언트에는 연결 끊김 한 줄만 보이고 원인은 두 겹으로 가려집니다.

터미널에서 손으로 실행하면 잘 뜨는데 에디터에서만 안 뜨는 증상이 나오면, 설정을 고치기 전에 두 환경의 PATH부터 비교하는 편이 빠릅니다. 설정 파일은 멀쩡하고 실행되는 파일만 다른 경우가 있습니다.

오늘 붙일 두 줄과 다음에 정리할 것

전역 PATH를 건드릴 생각은 없고요, 진단 두 줄만 먼저 붙였습니다. 고치는 건 그다음입니다.

# 설치 직후 한 번 돌립니다. 두 경로가 같으면 정상입니다
import os, sys, sysconfig, shutil, subprocess

def ver(p):
    return subprocess.run([p, "--version"], capture_output=True, text=True).stdout.strip()

onpath = shutil.which("uv")
print("PATH에서 잡히는 것 :", onpath, "→", ver(onpath) if onpath else "-")

import uv
print("find_uv_bin()     :", uv.find_uv_bin(), "→", ver(uv.find_uv_bin()))

user = os.path.join(
    sysconfig.get_path("scripts", scheme=sysconfig.get_preferred_scheme("user")), "uv")
print("user scripts      :", user, "→", ver(user) if os.path.isfile(user) else "없음")

이 환경에서는 세 줄이 각각 0.12.3 / 0.12.3 / 0.12.13으로 갈렸습니다. 셋이 같은 값이면 손댈 게 없고요, 갈렸다면 아래 표에서 하나를 고르는 순서입니다.

이름만 바꾸면 ruff나 다른 Astral 도구에도 그대로 씁니다. 팀에서 돌릴 거라면 셋 중 하나라도 다를 때 종료 코드를 1로 내도록 세 줄 더 붙여 셋업 스크립트 끝에 두는 편이 낫습니다.

고치는 방법과 비용

방법 도입 난이도 유지보수 비용 팀 학습곡선
~/.local/bin을 PATH 앞으로 낮음 (rc 파일 한 줄) 중간 — 다른 도구의 우선순위까지 같이 바뀝니다 낮음
프로젝트 venv 안에서만 설치 낮음 낮음 — venv가 사라지면 같이 사라집니다 낮음
공식 설치 스크립트로 일원화 후 uv self update 중간 — 기존 pip 설치본 제거가 선행 낮음 중간
설정 파일에 절대 경로 고정 낮음 높음 — 경로가 바뀌면 전부 손봐야 합니다 낮음

venv를 쓰면 탐색 1번이 venv/bin을 가리켜 순서가 자연히 뒤집힙니다. 실제로 venv 안에서 재현해 보니 find_uv_bin()이 venv/bin/uv를 반환했고 python -m uv도 0.12.13이 나왔습니다. 저는 도구 실행 환경이 섞이는 걸 줄이려고 이쪽을 골랐습니다.

PATH 앞당기기를 택하지 않은 이유는 효과가 uv 하나에 머물지 않아서입니다. ~/.local/bin을 맨 앞에 두면 거기 있는 모든 실행 파일이 시스템 것을 이깁니다. 지금 문제는 풀리지만 나중에 다른 도구에서 같은 종류의 혼선이 생겼을 때 원인을 한 겹 더 파야 합니다.

이 글의 범위

  • 컨테이너 리눅스(aarch64)에서 시스템 파이썬으로 재현한 결과입니다. macOS 프레임워크 파이썬은 user scheme이 달라 경로가 다르게 잡힙니다.
  • 0.12.3과 0.12.13의 동작 차이는 릴리스 노트로만 확인했고, 실행 결과가 갈리는 사례를 직접 만들지는 못했습니다.
  • 위 진단 스크립트는 uv 파이썬 패키지가 설치돼 있어야 두 번째 줄이 동작합니다.

pip install이 성공을 찍었다는 건 파일이 어딘가에 놓였다는 뜻이지, 당신이 칠 명령이 그 파일을 부른다는 뜻은 아니었습니다. 패키지 매니저가 관리하는 것은 설치 위치까지고, 무엇이 실행될지는 셸이 정합니다. 저는 이걸 알아내는 데 30분을 썼습니다만, 진단 두 줄이면 30초입니다.


댓글

이 블로그의 인기 게시물

옵시디언 3기기 동기화, 유료 결제 없이 구글 드라이브로 끝내기

최저가 주유소, 얼마나 돌아가면 손해일까?

회의시간 줄이는 법, 개수보다 배치 먼저

유류세 인하 2개월 연장, 주유소 가격 실제 인하폭 계산

국민연금 추납 조회, 미루면 얼마나 더 낼까?