default
값의 이상한 동작default
값으로 빌드를 시도하고, 실패하면 의외로 default값을 뒤집어서 시도합니다.verbosity=3
으로 리졸빙이 어찌 되가나 확인하기아래는 장황하긴 한데, 빌드 오류 이유를 찾는 방법을 일부 볼 수 있어, 시도했던 방법 그대로 기록했습니다.
@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가 어떻게 빌드 될지 현재 프로젝트에서 직접 제어할 방법이 없습니다.)
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로 버전을 맞춰 줍니다.
ghcup
에서 현재 활성화 버전을 set
할 수 있습니다.이렇게 맞추고 난 뒤에도 에러가 난다면, 패키지 버전 문제일 확률이 높습니다. 위 예시의 경우
- 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가 포함되어 있습니다.
Options
타입 생성자 또는 클래스가 없다고 하니, Hackage에서 어디에 속한 것인지 살펴 봤습니다. 여러 패키지 목록이 나오는데, 이 중 아마도 JSON을 다루는 aeson
에 속한 Options
로 추측할 수 있습니다. aeson
이 import
되지 않고 있거나, 현재 리졸브된 버전이 Options
이 없는 aeson
버전일 수도 있습니다.Options
로 문자열 검색을 해 보니,1.2.0.0
버전에 Options
생성자를 더 이상 드러내지expose 않는 다는 항목이 보입니다. 이 후 버전으로 리졸브됐기 때문에 Options
를 찾지 못한다는 에러가 나온 것으로 추측 할 수 있습니다. .cabal
파일의 build-depends
를 보니, 따로 aeson
버전 지정이 없습니다.※ 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
으로 리졸브되고 빌드 실패했습니다. 위에서 이슈가 있던 버전들과 어떤 상관 관계가 보이지 않아 난감했습니다.)
덕분에 얻은 힌트는 한가지, warp
는 3.3.25
에서 3.3.26
으로 올라가면서 cryptonite
를 crypton
으로 대체했다는 정보입니다.
이 후, 위에서 한 것처럼 verbosity=3
으로 로그를 쫓아가며 이유를 확인했습니다.
※ crypton
이 빌드가 안되었던 이유는
if impl(ghc < 8.8)
Buildable: False
kazu-yamamoto/crypton의 cabal 파일
위와 같이 되어 있어 GHC-8.6.5에서 빌드가 안됩니다. - @jhhuh
※ warp-3.3.27
은 3.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.1
은 primitive-0.8.0.0
아래 버전(aeson => primitive>=0.6.3.0 && < 0.8
)을 써야 해서 충돌이 발생합니다. 결과적으로 aeson
을 버전을 낮추게 됩니다.