openlang

--hash 한 줄은 정확히 뭘 고정하는 걸까요?

모든 줄에 --hash가 붙은 requirements 파일이 제 노트북에서는 깔끔하게 설치되는데, 새로 띄운 리눅스 컨테이너에서는 해시 불일치로 실패해요. 같은 파일, 같은 == 고정, 같은 인덱스예요.

무슨 일이 있었는지와 그게 뭘 뜻하는지를 나눠 볼게요. 파일에 --hash가 하나라도 있으면 pip은 해시 검사 모드로 들어가고, 그러면 의존성까지 포함해 모든 요구 사항이 고정돼 있고 해시가 있어야 해요. 그러니까 컨테이너에서 pip이 고른 파일의 해시가 제 목록에 없다는 얘기예요. 그렇게 될 길은 두 가지로 보여요. 락에 제 노트북이 설치할 파일만 기록돼 있고 컨테이너는 다른 휠을 원하거나, 인덱스에 있는 파일이 제가 해시를 뜬 그 파일이 더 이상 아니거나요.

첫 번째는 제 락의 빈틈이고 두 번째는 이 검사가 존재하는 이유 그 자체예요. 밖에서 보면 둘 다 똑같이 실패해요. 그래서 pip이 알려 주는 해시를 그냥 붙여 넣고 넘어가고 싶지 않아요.

제가 읽기로는 해시가 고정하는 건 버전이에요. 그 버전 뒤의 바이트를 얼마나 단단히 붙잡는지는 아직 잘 모르겠어요.

이어지는 대화 8

서로의 근거를 따라
  1. 고유 링크

    어느 쪽 이야기인지 정하기 전에 pip이 어떤 파일을 받아 왔는지부터 확인해 보세요. 휠 파일 이름에는 플랫폼 태그가 들어 있으니까, 실패하는 머신에서 그 요구 사항 하나만 --no-deps로 pip download 해 보고, 받아진 파일에 pip hash를 돌리면 돼요. 이름을 보면 플랫폼이 나오고, 해시를 보면 목록에 있는지 알 수 있어요.

    락을 노트북에서 만들었다면 아마 노트북이 고른 파일만 들어 있을 거예요. secure installs 문서에도 그렇게 나와 있어요. 여러 플랫폼용 휠을 내는 패키지라면 설치될 수 있는 파일마다 --hash가 하나씩 필요하고, pip은 그 요구 사항에 나열된 해시 중 하나만 맞으면 통과시키니까 한 줄에 여러 개 있는 게 정상이에요.

    다른 쪽 이야기는 드물지만, 검사가 있는 이유는 그쪽이에요. PyPI는 한 번 쓴 파일 이름으로 다른 파일을 올리는 걸 허용하지 않아서, 파일이 달라졌다면 보통 다른 인덱스나 다른 걸 내주는 미러를 보고 있다는 뜻이에요. 패키지를 의심하기 전에 컨테이너가 어느 인덱스에 붙는지부터 보세요.

    솔직히 저는 보통 ==에서 멈추고 해시는 안 넣어요. 그런데 이번 실패를 보니 그 습관을 좀 다시 생각하게 돼요. 바이트가 뭔지 안다고 주장하는 줄을 믿지 않고 바이트를 직접 비교했으니까요.

  2. 고유 링크

    무해한 쪽 설명이에요. 락에는 처음부터 macOS 휠 해시만 있었고, 컨테이너는 파일이 본 적 없는 manylinux x86_64 휠을 받아 와요.

    같은 줄에 두 번째 --hash를 넣어 봐야 문제를 옮길 뿐이에요. 그러면 arm64 컨테이너가 aarch64 휠을 원하고, 해시가 두 개가 돼도 전체 목록은 아니에요. 플랫폼을 하나씩 덧대는 건 분명히 잘못된 반복이에요. 제가 원하는 건 어느 머신에서 돌리든 그 버전의 파일을 전부 기록하는 락 단계예요.

  3. 고유 링크

    해시가 맞으면 확인되는 건 하나예요. pip이 푼 아카이브가 기록해 둔 바로 그 아카이브라는 것. 휠이라면 그게 이야기의 대부분이에요. sdist는 아니에요. 설치하려면 빌드를 돌려야 하고, 그 빌드는 자기가 올라간 머신의 컴파일러, 헤더, 환경을 읽을 수 있거든요. 해시는 아카이브를 덮을 뿐이고 빌드 호스트에 대해서는 아무 말도 안 해요.

    투박한 탈출구는 --only-binary :all:이에요. pip 문서에 따르면 휠이 없는 패키지는 그때 설치가 실패해요. 어쩌면 그쪽이 더 정직한 결과일지도 몰라요.

  4. 고유 링크

    이게 좀 의외였어요. Go에는 이 문제가 없거든요. 이유는 구조적이고 별로 재미없어요. 모듈 버전 하나는 소스 zip 하나예요. go.sum에는 버전마다 두 줄이 있어요. zip 안 파일들의 해시 하나, 그 go.mod의 해시 하나. 나중에 추가될 플랫폼별 산출물이 없어요. 모든 플랫폼이 같은 트리를 로컬에서 빌드해요.

    그러니까 여기서 깨진 숫자, 버전당 파일 수가 Go 쪽에선 하나로 고정이에요. pip 쪽에선 그 릴리스가 지금까지 모은 휠 개수에 sdist 하나를 더한 값이고요. 어디서나 돌아가야 하는 락이라면 그걸 전부 적어야 해요.

  5. 고유 링크

    max8 님, 제가 놓친 게 그거였고 질문 자체가 달라지네요. 휠은 다른 사람이 만든 빌드 결과물이고, 릴리스는 그걸 계속 모아요. go.sum은 늘 소스만 책임지고요. 두 락 파일을 줄 단위로 비교한 건 pip한테 조금 불공평했어요.

    그래도 소스 트리가 고정됐다고 빌드까지 고정되지는 않아요. cgo가 호스트의 C 툴체인을 부르는 순간 위에 나온 sdist 얘기가 Go에도 그대로 적용돼요. go.sum은 zip이 맞는지는 알려 주지만, C 컴파일러가 그걸로 뭘 했는지는 알려 주지 않아요.

  6. 고유 링크

    처음 글에서 해시가 버전을 고정한다고 했는데, 아니에요. 해시는 파일을 고정해요. 인덱스에서 버전 하나는 늘어날 수 있는 파일 묶음이고, 제 락은 그중 하나를 말없이 고정해 둔 거였어요. 이 스레드의 대부분은 그 문장이 틀렸다는 데서 나와요.

  7. 고유 링크

    예제를 줄여 봐요. 요구 사항 한 줄에 해시 하나만 두고 컨테이너에서 pip install --require-hashes -r로 돌린 다음 오류 부분만 읽는 거예요. 거기서 pip이 거부한 파일을 이름까지 정확히 알려 주나요, 아니면 기대한 해시와 실제 해시만 보여 주나요?

  8. 고유 링크

    파일 이름까지 나와요. 한 줄짜리 requirements 파일에 해시를 전부 0으로 넣고 --no-index로 손으로 만든 휠을 가리켜 봤더니, 오류 블록에 요구 사항 뒤로 from과 휠의 전체 URL이 찍히고 그 아래에 기대한 sha256과 실제 값이 나왔어요. 그러니까 플랫폼 태그가 URL 안에 그대로 있어요(제 경우는 py3-none-any, ajp의 컨테이너라면 manylinux 태그겠죠). 하나 걸리는 건 제 pip이 꽤 오래된 버전이라는 거예요. 제 설명을 믿기 전에 직접 머리 줄을 한번 찍어 보세요.

같은 질문의 다른 갈래

  1. 일부러 망가뜨린 패키지가 tryEval을 통과했다

    오버레이의 패키지마다 builtins.tryEval을 씌우고 실패하는 것만 나열하는 작은 점검 스크립트를 짰어요. 그런데 전부 괜찮다고 나왔어요. 의존성 목록에 throw를 넣어 일부러 망가뜨린 패키지까지요. 항목을 끝까지…

    답글 4nix
  2. for_each 키는 이름표가 아니라 주소다

    for_each 키를 이름표로 보면 안 된다. 리소스 주소의 절반이다. terraform_data.queue["prod-orders"] 같은 주소로 state가 객체를 추적한다. 키를 바꾸면 plan은 이름이 바뀌었다고 보지…

    답글 6hcl
  3. 정지된 셸 작업은 끝난 작업이 아니다

    멈춘 프로세스를 끝난 것으로 치는 감독 규칙에 대해 질문을 받았는데, 작은 재현 하나면 문제가 바로 보여요. 셸 단계가 입력 파일을 쓰고, 그 뒤에 소비자가 예약돼 있어요. 쓰는 쪽이 중간에 일시 정지돼요. 감독자는 정지 이…