Cabal 빌드 플래그

Posted on July 12, 2023

요약

아래는 장황하긴 한데, 빌드 오류 이유를 찾는 방법을 일부 볼 수 있어, 시도했던 방법 그대로 기록했습니다.

@jhhuh님이 Cabal의 기본 개념을 알려주시고, 목적지에 도달할 수 있게, 귀한 시간을 들여 저를 “드리블” 해주셨습니다. ;-) 감사합니다. 혹시 틀린 부분이 있다면, 제가 잘못 정리한 것으로, 아직 따로 리뷰를 통해 검증 받은 글은 아닙니다.

분명 예시 공개 시점에선 빌드 성공했을 것

2023.6 현재 jose-scotty 예시 파일은 예시에서 시키는대로 하면 빌드가 안됩니다.

Network/Wai/Handler/Warp/Types.hs:10:1: error:
    Could not load module ‘Data.X509’
    It is a member of the hidden package ‘x509-1.7.7’.
    Perhaps you need to add ‘x509’ to the build-depends in your .cabal file.
    Use -v to see a list of the files searched for.
10 | import Data.X509
   |

분명 예시는 빌드가 됐을텐데, 어째서 모듈 지정 자체가 안되어 있다는 오류가 날까요?

보통 hidden 에러가 뜨면, 위 에러 설명에서 제안하는 대로 .cabal 파일에 dependency를 잡아 줍니다.

executable jwt-scotty
  build-depends:       base >=4.12 && <5
                     , x509
                     , ...

하지만, 이렇게 해도 동일한 에러가 뜨고, 게다가 아래 에러가 추가 됐습니다.

[1 of 9] Compiling Jose.Jwa         ( Jose/Jwa.hs, dist/build/Jose/Jwa.o )
[2 of 9] Compiling Jose.Types       ( Jose/Types.hs, dist/build/Jose/Types.o )

Jose/Types.hs:144:18: error:
    Not in scope: type constructor or class ‘Options’
144 | claimsOptions :: Options
    |                  ^^^^^^^

Jose/Types.hs:190:15: error:
    Not in scope: type constructor or class ‘Options’
190 | jwsOptions :: Options
    |               ^^^^^^^
...

예시를 만들 당시엔 x509를 별도로 모듈 지정을 안해도 됐다는 건, 다른 라이브러리가 안에 가지고 있던 건 아닐까 추측했는데, 한 가지 경우의 수가 더 있었습니다. 조건부 빌드를 위해 플래그를 써서 모듈을 지정하는 방식이 있습니다.

추가된 Not in scope... 오류말고, 원 소스 그대로 상태에서 발생한 Could not load module... 오류를 보겠습니다.

다음과 같은 이유를 추측해 볼 수 있습니다.
dependency에 잡아 주었지만, 어떤 절차에선가 강제로 다시 dependency에서 뺀다.”
왜냐하면 여전히 패키지를 못 찾는다는 같은 오류가 나기 때문입니다. 어째서 이런 일이 생길까요?

보통 Cabal, Stack을 매뉴얼을 통독하며 익히는 경우는 드문 것 같습니다. 그저 빌드가 되면, 세부 기능은 뒤로 미룹니다. 위 예시의 빌드 오류에는 Cabal 매뉴얼을 따로 보지 않으면 알 수 없는 플래그 관련 동작이 있습니다.

플래그

참고 - GHC 플래그 가이드
조건부로 다르게 빌드할 필요가 있을 때 플래그를 정의할 수 있습니다.(추측: 현재 프로젝트가 A라는 라이브러리를 갖다 쓸 때, A라이브러리를 특정 조건에 따라 빌드를 달리 하려면, 플래그를 통해 제어하는 것으로 보입니다. 플래그가 없다면, A가 어떻게 빌드 될지 현재 프로젝트에서 직접 제어할 방법이 없습니다.)

Warp 플래그

hackage에서 Warp 패키지 정보를 보면 4개의 플래그를 확인할 수 있습니다.

network-bytestring (default: Disabled)
allow-sendfilefd (default: Enabled)
warp-debug (default: Disabled)
x509 (default: Enabled)

직접 Warp 패키지 빌드에 쓰이는 cabal파일을 보고 확인할 수도 있습니다.
Warp cabal 파일

Flag x509
    Description: Adds a dependency on the x509 library to enable getting TLS client certificates.
    Default:     True

  if flag(x509)
      Build-Depends: crypton-x509

x509 플래그를 True로 지정하면, 의존성에 crypton-x509를 추가하는 게 보입니다. 플래그 값 지정은 +True, -False를 지정합니다.

cabal.project.local+x509를 명시적으로 넣어 주면 빌드에 성공합니다.(현재 2023.6)

그런데, 위에 보면 x509는 디폴트로 Enabled인데, 아래와 같이 왜 또 constraints에 지정해줘야 할까요?

with-compiler: ghc-8.6.5
constraints: warp +x509

이런 이상해 보이는 동작을 알려면 Cabal의 “독특한” 디폴트 동작을 알아야 합니다. (어찌 보면 비상식적으로 보이는 동작입니다.)

빌드할 때 verbosity=3을 줘서 아래 로그를 얻을 수 있습니다.

여기 ----> [156] trying: warp:+x509 
[157] trying: crypton-x509-1.7.6 (dependency of warp +x509 *test)
[158] trying: crypton-x509:!test
[159] trying: pem-0.2.4 (dependency of crypton-x509)
[160] trying: pem:!test
[161] next goal: memory (dependency of crypton-x509)
[161] rejecting: memory-0.18.0 (library is not buildable in the current environment, but it is required by pem)
[161] trying: memory-0.17.0
[162] trying: memory:!test
[163] trying: memory:+support_deepseq
[164] trying: memory:+support_bytestring
[165] trying: hourglass-0.2.12 (dependency of crypton-x509)
[166] trying: hourglass:!bench
[167] trying: hourglass:!test
[168] next goal: crypton (dependency of crypton-x509)
[168] rejecting: crypton-0.31 (library is not buildable in the current environment, but it is required by crypton-x509)
[168] fail (backjumping, conflict set: crypton, crypton-x509)
[157] fail (backjumping, conflict set: crypton, crypton-x509, warp, warp:x509, warp:test)
여기 ----> [156] trying: warp:-x509

디폴트 옵션 +x509로 빌드 시도하고, crypton-0.31 라이브러리 빌드에 실패하고 플래그를 -x509로 뒤집는negate게 보입니다.
반드시 디폴트 플래그로 빌드해야 하는 게 아니라, “우선” 디폴트로 빌드를 시도합니다.

그런데, 만일 constraints에 명시적으로 +x509를 추가하면 아래와 같은 로그를 볼 수 있습니다.

여기 ----> [254] trying: warp:+x509
[255] trying: crypton-x509-1.7.6 (dependency of warp +x509)
[256] trying: crypton-x509:!test
[257] trying: pem-0.2.4 (dependency of crypton-x509)
[258] trying: pem:!test
[259] trying: hourglass-0.2.12 (dependency of crypton-x509)
[260] trying: hourglass:!bench
[261] trying: hourglass:!test
[262] next goal: crypton (dependency of crypton-x509)
[262] rejecting: crypton-0.31 (library is not buildable in the current environment, but it is required by crypton-x509)
[262] fail (backjumping, conflict set: crypton, crypton-x509)
[255] fail (backjumping, conflict set: crypton, crypton-x509, warp, warp:x509)
여기 ----> [254] rejecting: warp:-x509 (constraint from project config /home/jacoo/gitProjects/jisimsim/reference/jwt-scotty/cabal.project.local requires opposite flag selection)
[254] fail (backjumping, conflict set: crypton, crypton-x509, warp, warp:x509)
[148] trying: warp-3.3.26
여기 ----> [149] trying: warp:+x509
[150] trying: crypton-x509-1.7.6 (dependency of warp +x509)
[151] next goal: crypton (dependency of crypton-x509)
[151] rejecting: crypton-0.31 (library is not buildable in the current environment, but it is required by crypton-x509)
[151] fail (backjumping, conflict set: crypton, crypton-x509)
[150] fail (backjumping, conflict set: crypton, crypton-x509, warp, warp:x509)
여기 ----> [149] rejecting: warp:-x509 (constraint from project config /home/jacoo/gitProjects/jisimsim/reference/jwt-scotty/cabal.project.local requires opposite flag selection)
[149] fail (backjumping, conflict set: crypton, crypton-x509, warp, warp:x509)
[148] trying: warp-3.3.25
여기 ----> [149] trying: warp:+x509

디폴트 +x509로 시도하고, 실패하면 -x509로 바꾸려 하지만, constraints: +x509 제약이 그러지 못하도록 막습니다. 그래서, +x509 옵션은 그대로 두고, warp 버전을 낮춰가며 빌드 시도합니다. 바로 이 동작을 알아야, 위와 같은 상황이 왜 벌어지는지 이해할 수 있습니다.

디폴트로 지정하면, 그 값이 고정이 아니라, 우선 디폴트 값으로 빌드 시도하고, 실패하면 디폴트 값을 뒤집어 다시 시도합니다. 무슨 이유에선가 +x509로 실패해서 -x509로 시도하니, 아예 모듈 자체가 dependency에 잡히지 않았던 겁니다.

해결책을 찾기까지 시도했던 방법들

Cabal은 Stack처럼 스냅샷 방식이 아니라서, cabal freeze를 하지 않았다면, 항상 같은 리졸브 결과를 가지는 건 아닌 것으로 보입니다. (제약이 딱히 없다면, 가급적 최신으로 맞춰지는 것 같습니다.) 어찌됐든, 몇 년 전에는 멀쩡히 잘 돌아가서 올려놓은 예시일텐데, 지금 빌드가 되지 않는다면, 환경(cabal 버전, ghc 버전, 기타 의존하는 바이너리나 환경 변수…) 문제이거나, 패키지들 버전 문제입니다.

우선, 예시를 작성할 때 사용한 ghc, cabal로 버전을 맞춰 줍니다.

이렇게 맞추고 난 뒤에도 에러가 난다면, 패키지 버전 문제일 확률이 높습니다. 위 예시의 경우 - Warp패키지의 x509 관련 에러니, Warp의 change history를 살펴 x509관련 이슈가 있는지 살펴 봤습니다.
https://hackage.haskell.org/package/warp-3.3.28/changelog
3.3.20 버전에 x509 플래그가 추가된 것과, 3.3.28 버전에서 ‘-x509’ 플래그 관련 된 걸 고쳤다는 항목이 보입니다.
패키지 설명의 Dependencies에 보면 crypton-x509가 포함되어 있습니다.

※ cabal이 resolve한 패키지 버전들은 로그를 보거나, cabal build --dry-run 실행, dist-newstyle/cache/plan.json에서 확인할 수 있습니다.

(@jhhuh님이 도움을 주시면서 같이 테스트해 주셨는데, warp-3.3.25, aeson-1.4.7.1 버전으로 리졸브되어 빌드 성공했다고 알려 주셨습니다. 반면 저는 warp-3.3.26, aeson-0.11.3.0 으로 리졸브되고 빌드 실패했습니다. 위에서 이슈가 있던 버전들과 어떤 상관 관계가 보이지 않아 난감했습니다.)

덕분에 얻은 힌트는 한가지, warp3.3.25에서 3.3.26으로 올라가면서 cryptonitecrypton으로 대체했다는 정보입니다.

이 후, 위에서 한 것처럼 verbosity=3으로 로그를 쫓아가며 이유를 확인했습니다.

crypton이 빌드가 안되었던 이유는

  if impl(ghc < 8.8)
    Buildable: False

kazu-yamamoto/crypton의 cabal 파일
위와 같이 되어 있어 GHC-8.6.5에서 빌드가 안됩니다. - @jhhuh

warp-3.3.273.3.26까지와는 다르게 x509 플래그와 상관없이, x509 관련 코드가 포함되도록, 하드 코딩 되어 있는데, 저자가 의도한 것이 아니라면, 버그로 보입니다.

Options 에러는, Warp가 아닌, 현재 프로젝트에 x509 dependency를 추가하면, Options정의가 없는 aeson 버전을 잡게 되어 (aeson-0.11.3.0) 에러가 나는 걸로 보입니다. x509 dependency를 주지 않으면, aeson-1.4.7.1로 결정됩니다.

[232] rejecting: aeson-1.4.7.1 (conflict: primitive==0.8.0.0, aeson => primitive>=0.6.3.0 && <0.8)
[232] skipping: aeson-1.4.7.0, aeson-1.4.6.0, aeson-1.4.5.0, aeson-1.4.4.0, aeson-1.4.3.0, aeson-1.4.2.0, aeson-1.4.1.0 (has the same characteristics that caused the previous version to fail: excludes 'primitive' version 0.8.0.0)
[232] rejecting: aeson-1.4.0.0 (conflict: base-compat==0.12.2, aeson => base-compat>=0.9.1 && <0.11)
[232] skipping: aeson-1.3.1.1, aeson-1.3.1.0, aeson-1.3.0.0, aeson-1.2.4.0, aeson-1.2.3.0, aeson-1.2.2.0, aeson-1.2.1.0, aeson-1.2.0.0, aeson-1.1.2.0, aeson-1.1.1.0, aeson-1.1.0.0, aeson-1.0.2.1, aeson-1.0.2.0, aeson-1.0.1.0, aeson-1.0.0.0 (has the same characteristics that caused the previous version to fail: excludes 'base-compat' version 0.12.2)
[232] trying: aeson-0.11.3.0

예를 들면, x509 dependency가 있을 때는 primitive-0.8.0.0으로, 없을 때는 primitive-0.7.4.0으로 configured됩니다. aeson-1.4.7.1primitive-0.8.0.0 아래 버전(aeson => primitive>=0.6.3.0 && < 0.8)을 써야 해서 충돌이 발생합니다. 결과적으로 aeson을 버전을 낮추게 됩니다.

Github 계정이 없는 분은 메일로 보내주세요. lionhairdino at gmail.com