Code Loading
이 장에서는 패키지 로딩의 기술적 세부 사항을 다룹니다. 패키지를 설치하려면 Pkg
를 사용하여 Julia의 내장 패키지 관리자를 통해 활성 환경에 패키지를 추가하세요. 활성 환경에 이미 있는 패키지를 사용하려면 import X
또는 using X
를 작성하면 됩니다. 이는 Modules documentation에 설명되어 있습니다.
Definitions
줄리아는 코드를 로드하는 두 가지 메커니즘을 가지고 있습니다:
- 코드 포함: 예:
include("source.jl")
. 포함은 단일 프로그램을 여러 소스 파일로 나눌 수 있게 해줍니다. 표현식include("source.jl")
는include
호출이 발생하는 모듈의 전역 범위에서source.jl
파일의 내용을 평가하게 합니다.include("source.jl")
가 여러 번 호출되면,source.jl
은 여러 번 평가됩니다. 포함된 경로인source.jl
은include
호출이 발생하는 파일을 기준으로 상대적으로 해석됩니다. 이는 소스 파일의 하위 트리를 쉽게 이동할 수 있게 해줍니다. REPL에서는 포함된 경로가 현재 작업 디렉토리를 기준으로 해석됩니다,pwd()
. - 패키지 로딩: 예:
import X
또는using X
. 임포트 메커니즘은 패키지를 로드할 수 있게 해줍니다. 즉, 독립적이고 재사용 가능한 Julia 코드의 모음으로, 모듈에 래핑되어 있으며, 결과 모듈을 임포트하는 모듈 내에서X
라는 이름으로 사용할 수 있게 합니다. 동일한X
패키지가 같은 Julia 세션에서 여러 번 임포트되면, 첫 번째 로드된 경우에만 로드되고, 이후의 임포트에서는 임포트하는 모듈이 동일한 모듈에 대한 참조를 받습니다. 그러나import X
는 서로 다른 컨텍스트에서 서로 다른 패키지를 로드할 수 있다는 점에 유의해야 합니다:X
는 메인 프로젝트에서X
라는 이름의 하나의 패키지를 참조할 수 있지만, 각 의존성에서X
라는 이름의 서로 다른 패키지를 참조할 수도 있습니다. 이에 대한 자세한 내용은 아래에서 다룹니다.
코드 포함은 매우 간단하고 직관적입니다: 주어진 소스 파일을 호출자의 맥락에서 평가합니다. 패키지 로딩은 코드 포함 위에 구축되며 different purpose를 제공합니다. 이 장의 나머지 부분은 패키지 로딩의 동작과 메커니즘에 초점을 맞춥니다.
패키지는 다른 Julia 프로젝트에서 재사용할 수 있는 기능을 제공하는 표준 레이아웃을 가진 소스 트리입니다. 패키지는 import X
또는 using X
문을 통해 로드됩니다. 이러한 문은 또한 패키지 코드를 로드하여 생성된 X
라는 이름의 모듈을 import 문이 발생하는 모듈 내에서 사용할 수 있게 합니다. import X
에서 X
의 의미는 문맥에 따라 다릅니다: 어떤 X
패키지가 로드되는지는 문이 발생하는 코드에 따라 달라집니다. 따라서 import X
의 처리는 두 단계로 이루어집니다: 첫째, 이 문맥에서 어떤 패키지가 X
로 정의되는지를 결정합니다; 둘째, 그 특정 X
패키지가 어디에 있는지를 결정합니다.
이 질문들은 LOAD_PATH
에 나열된 프로젝트 환경을 검색하여 프로젝트 파일(Project.toml
또는 JuliaProject.toml
), 매니페스트 파일(Manifest.toml
또는 JuliaManifest.toml
, 또는 특정 버전의 경우 -v{major}.{minor}.toml
로 접미사가 붙은 동일한 이름) 또는 소스 파일 폴더를 통해 답변됩니다.
Federation of packages
대부분의 경우, 패키지는 이름만으로 고유하게 식별할 수 있습니다. 그러나 때때로 프로젝트는 동일한 이름을 가진 두 개의 서로 다른 패키지를 사용해야 하는 상황에 직면할 수 있습니다. 이러한 경우 중 하나의 패키지 이름을 변경하여 문제를 해결할 수 있지만, 그렇게 강제로 변경해야 하는 것은 대규모 공유 코드베이스에서 매우 방해가 될 수 있습니다. 대신, Julia의 코드 로딩 메커니즘은 동일한 패키지 이름이 애플리케이션의 서로 다른 구성 요소에서 서로 다른 패키지를 참조할 수 있도록 허용합니다.
줄리아는 연합 패키지 관리를 지원합니다. 이는 여러 독립적인 당사자가 공용 및 개인 패키지와 패키지 레지스트리를 유지할 수 있으며, 프로젝트가 서로 다른 레지스트리의 공용 및 개인 패키지를 혼합하여 의존할 수 있음을 의미합니다. 다양한 레지스트리의 패키지는 공통의 도구 및 워크플로를 사용하여 설치 및 관리됩니다. 줄리아와 함께 제공되는 Pkg
패키지 관리자는 프로젝트의 의존성을 설치하고 관리할 수 있게 해줍니다. 이는 프로젝트 파일(프로젝트가 의존하는 다른 프로젝트를 설명하는 파일)과 매니페스트 파일(프로젝트의 전체 의존성 그래프의 정확한 버전을 스냅샷하는 파일)을 생성하고 조작하는 데 도움을 줍니다.
연합의 한 결과는 패키지 이름에 대한 중앙 권한이 존재할 수 없다는 것입니다. 서로 다른 엔티티가 관련 없는 패키지를 지칭하기 위해 동일한 이름을 사용할 수 있습니다. 이러한 가능성은 피할 수 없으며, 이러한 엔티티는 조정하지 않거나 서로에 대해 알지 못할 수도 있습니다. 중앙 이름 권한이 없기 때문에 단일 프로젝트는 동일한 이름을 가진 서로 다른 패키지에 의존하게 될 수 있습니다. Julia의 패키지 로딩 메커니즘은 패키지 이름이 단일 프로젝트의 의존성 그래프 내에서도 전 세계적으로 고유할 필요가 없습니다. 대신, 패키지는 universally unique identifiers (UUID)로 식별되며, 각 패키지가 생성될 때 할당됩니다. 일반적으로 Pkg
가 이를 생성하고 추적해 주기 때문에 이러한 다소 번거로운 128비트 식별자와 직접 작업할 필요는 없습니다. 그러나 이러한 UUID는 "패키지 X
는 무엇을 참조하는가?"라는 질문에 대한 확실한 답을 제공합니다.
탈중앙화된 네이밍 문제는 다소 추상적이기 때문에, 문제를 이해하기 위해 구체적인 시나리오를 살펴보는 것이 도움이 될 수 있습니다. App
이라는 애플리케이션을 개발하고 있다고 가정해 보겠습니다. 이 애플리케이션은 두 개의 패키지인 Pub
과 Priv
를 사용합니다. Priv
는 당신이 만든 비공식 패키지이고, Pub
은 당신이 사용하지만 제어하지 않는 공개 패키지입니다. Priv
를 만들었을 때, Priv
라는 이름의 공개 패키지는 존재하지 않았습니다. 그러나 이후에 관련 없는 패키지인 Priv
가 출판되어 인기를 끌게 되었습니다. 사실, Pub
패키지가 이 패키지를 사용하기 시작했습니다. 따라서 다음에 Pub
을 업그레이드하여 최신 버그 수정 및 기능을 얻으면, App
은 두 개의 서로 다른 Priv
패키지에 의존하게 됩니다. 이는 업그레이드 외에는 당신의 어떤 행동도 필요하지 않습니다. App
은 당신의 비공식 Priv
패키지에 직접 의존하고, Pub
을 통해 새로운 공개 Priv
패키지에 간접적으로 의존하게 됩니다. 이 두 Priv
패키지는 다르지만 App
이 올바르게 작동하기 위해서는 둘 다 필요하므로, import Priv
라는 표현은 App
의 코드에서 발생하느냐, Pub
의 코드에서 발생하느냐에 따라 서로 다른 Priv
패키지를 참조해야 합니다. 이를 처리하기 위해, Julia의 패키지 로딩 메커니즘은 UUID를 통해 두 Priv
패키지를 구별하고, 그 맥락(즉, import
를 호출한 모듈)에 따라 올바른 패키지를 선택합니다. 이러한 구별이 어떻게 작동하는지는 다음 섹션에서 설명하는 환경에 의해 결정됩니다.
Environments
환경은 다양한 코드 컨텍스트에서 import X
와 using X
가 의미하는 바와 이러한 문장이 어떤 파일을 로드하게 되는지를 결정합니다. Julia는 두 가지 종류의 환경을 이해합니다:
- 프로젝트 환경은 프로젝트 파일과 선택적 매니페스트 파일이 있는 디렉토리이며, 명시적 환경을 형성합니다. 프로젝트 파일은 프로젝트의 직접 종속성의 이름과 정체성을 결정합니다. 매니페스트 파일이 있는 경우, 모든 직접 및 간접 종속성을 포함한 완전한 종속성 그래프, 각 종속성의 정확한 버전, 그리고 올바른 버전을 찾고 로드하는 데 필요한 충분한 정보를 제공합니다.
- 패키지 디렉토리는 하위 디렉토리로서 일련의 패키지의 소스 트리를 포함하는 디렉토리이며, 암묵적 환경을 형성합니다. 만약
X
가 패키지 디렉토리의 하위 디렉토리이고X/src/X.jl
이 존재한다면, 패키지 디렉토리 환경에서 패키지X
를 사용할 수 있으며X/src/X.jl
은 그것이 로드되는 소스 파일입니다.
이들은 스택 환경을 생성하기 위해 혼합될 수 있습니다: 프로젝트 환경과 패키지 디렉토리의 정렬된 집합이 겹쳐져 단일 복합 환경을 만듭니다. 그런 다음 우선 순위 및 가시성 규칙이 결합되어 어떤 패키지가 사용 가능한지와 어디에서 로드되는지를 결정합니다. 예를 들어, 줄리아의 로드 경로는 스택 환경을 형성합니다.
이러한 환경은 각각 다른 목적을 가지고 있습니다:
- 프로젝트 환경은 재현성을 제공합니다. 프로젝트 환경을 버전 관리에 체크인함으로써—예: git 저장소—프로젝트의 소스 코드와 함께 프로젝트의 정확한 상태와 모든 종속성을 재현할 수 있습니다. 특히 매니페스트 파일은 모든 종속성의 정확한 버전을 캡처하며, 이는 소스 트리의 암호화 해시로 식별되어
Pkg
가 올바른 버전을 검색하고 모든 종속성에 대해 기록된 정확한 코드를 실행하고 있는지 확신할 수 있게 합니다. - 패키지 디렉토리는 전체적으로 신중하게 추적된 프로젝트 환경이 필요하지 않을 때 편리함을 제공합니다. 패키지 세트를 어딘가에 두고 프로젝트 환경을 만들 필요 없이 직접 사용할 수 있을 때 유용합니다.
- 스택 환경은 도구를 기본 환경에 추가할 수 있게 해줍니다. 개발 도구의 환경을 스택의 끝에 푸시하여 REPL 및 스크립트에서 사용할 수 있도록 할 수 있지만, 패키지 내부에서는 사용할 수 없습니다.
높은 수준에서 각 환경은 개념적으로 세 가지 맵을 정의합니다: 루트, 그래프 및 경로. import X
의 의미를 해석할 때, 루트와 그래프 맵은 X
의 정체성을 결정하는 데 사용되며, 경로 맵은 X
의 소스 코드를 찾는 데 사용됩니다. 세 가지 맵의 구체적인 역할은 다음과 같습니다:
루트:
name::Symbol
⟶uuid::UUID
환경의 루트 맵은 환경이 메인 프로젝트에 제공하는 모든 최상위 종속성에 대해 패키지 이름을 UUID에 할당합니다(즉,
Main
에서 로드할 수 있는 것들). 줄리아가 메인 프로젝트에서import X
를 만났을 때,X
의 정체성을roots[:X]
로 조회합니다.그래프:
context::UUID
⟶name::Symbol
⟶uuid::UUID
환경의 그래프는 각
context
UUID에 대해 이름에서 UUID로의 맵을 할당하는 다단계 맵입니다. 이는 루트 맵과 유사하지만 해당context
에 특정적입니다. Julia가 UUID가context
인 패키지의 코드에서import X
를 볼 때,X
의 정체성을graph[context][:X]
로 조회합니다. 특히, 이는import X
가context
에 따라 서로 다른 패키지를 참조할 수 있음을 의미합니다.경로:
uuid::UUID
×name::Symbol
⟶path::String
경로 맵은 각 패키지 UUID-이름 쌍에 대해 해당 패키지의 진입점 소스 파일의 위치를 할당합니다.
import X
에서X
의 정체성이 루트 또는 그래프를 통해 UUID로 확인된 후(주 프로젝트에서 로드되었는지 또는 종속성에서 로드되었는지에 따라), Julia는 환경에서paths[uuid,:X]
를 조회하여X
를 가져오기 위해 로드할 파일을 결정합니다. 이 파일을 포함하면X
라는 이름의 모듈이 정의되어야 합니다. 이 패키지가 로드되면, 동일한uuid
로 해결되는 이후의 모든 import는 이미 로드된 패키지 모듈에 대한 새로운 바인딩을 생성합니다.
각 종류의 환경은 다음 섹션에 자세히 설명된 대로 이 세 가지 맵을 다르게 정의합니다.
이 장의 예제는 루트, 그래프 및 경로에 대한 전체 데이터 구조를 보여줍니다. 그러나 줄리아의 패키지 로딩 코드는 이를 명시적으로 생성하지 않습니다. 대신, 필요한 패키지를 로드하기 위해 각 구조체의 필요한 부분만 지연 계산합니다.
Project environments
프로젝트 환경은 Project.toml
이라는 프로젝트 파일을 포함하는 디렉토리에 의해 결정되며, 선택적으로 Manifest.toml
이라는 매니페스트 파일을 포함할 수 있습니다. 이러한 파일은 JuliaProject.toml
및 JuliaManifest.toml
이라고도 불릴 수 있으며, 이 경우 Project.toml
및 Manifest.toml
은 무시됩니다. 이는 Project.toml
및 Manifest.toml
이라는 파일을 중요하게 여기는 다른 도구와의 공존을 허용합니다. 그러나 순수 Julia 프로젝트의 경우, Project.toml
및 Manifest.toml
이라는 이름이 선호됩니다. 그러나 Julia v1.10.8 이후부터는 (Julia)Manifest-v{major}.{minor}.toml
형식이 인식되어 특정 매니페스트 파일을 사용하도록 특정 Julia 버전을 설정할 수 있습니다. 즉, 동일한 폴더 내에서 Manifest-v1.11.toml
은 v1.11에 의해 사용되고, Manifest.toml
은 다른 모든 Julia 버전에 의해 사용됩니다.
프로젝트 환경의 루트, 그래프 및 경로 맵은 다음과 같이 정의됩니다:
환경의 루트 맵은 프로젝트 파일의 내용에 의해 결정되며, 특히 최상위 name
및 uuid
항목과 [deps]
섹션(모두 선택 사항)입니다. 앞서 설명한 가상의 애플리케이션 App
에 대한 다음 예제 프로젝트 파일을 고려해 보십시오:
name = "App"
uuid = "8f986787-14fe-4607-ba5d-fbff2944afa9"
[deps]
Priv = "ba13f791-ae1d-465a-978b-69c3ad90f72b"
Pub = "c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1"
이 프로젝트 파일은 다음과 같은 루트 맵을 나타냅니다. 만약 이것이 줄리아 사전으로 표현된다면:
roots = Dict(
:App => UUID("8f986787-14fe-4607-ba5d-fbff2944afa9"),
:Priv => UUID("ba13f791-ae1d-465a-978b-69c3ad90f72b"),
:Pub => UUID("c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1"),
)
주어진 루트 맵에서 App
의 코드에서 import Priv
문은 Julia가 roots[:Priv]
를 조회하게 하며, 이는 Priv
패키지의 UUID인 ba13f791-ae1d-465a-978b-69c3ad90f72b
를 반환합니다. 이 UUID는 메인 애플리케이션이 import Priv
를 평가할 때 어떤 Priv
패키지를 로드하고 사용할지를 식별합니다.
프로젝트 환경의 의존성 그래프는 존재하는 경우 매니페스트 파일의 내용에 의해 결정됩니다. 매니페스트 파일이 없으면 그래프는 비어 있습니다. 매니페스트 파일은 프로젝트의 직접 또는 간접 의존성 각각에 대한 구문을 포함합니다. 각 의존성에 대해 파일은 패키지의 UUID와 소스 코드에 대한 소스 트리 해시 또는 명시적 경로를 나열합니다. 다음은 App
에 대한 예제 매니페스트 파일입니다:
[[Priv]] # the private one
deps = ["Pub", "Zebra"]
uuid = "ba13f791-ae1d-465a-978b-69c3ad90f72b"
path = "deps/Priv"
[[Priv]] # the public one
uuid = "2d15fe94-a1f7-436c-a4d8-07a9a496e01c"
git-tree-sha1 = "1bf63d3be994fe83456a03b874b409cfd59a6373"
version = "0.1.5"
[[Pub]]
uuid = "c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1"
git-tree-sha1 = "9ebd50e2b0dd1e110e842df3b433cb5869b0dd38"
version = "2.1.4"
[Pub.deps]
Priv = "2d15fe94-a1f7-436c-a4d8-07a9a496e01c"
Zebra = "f7a24cb4-21fc-4002-ac70-f0e3a0dd3f62"
[[Zebra]]
uuid = "f7a24cb4-21fc-4002-ac70-f0e3a0dd3f62"
git-tree-sha1 = "e808e36a5d7173974b90a15a353b564f3494092f"
version = "3.4.2"
이 매니페스트 파일은 App
프로젝트에 대한 가능한 전체 의존성 그래프를 설명합니다:
- 애플리케이션에서 사용하는 두 가지 다른 패키지 이름이
Priv
입니다. 하나는 루트 의존성인 비공식 패키지이고, 다른 하나는Pub
을 통해 간접 의존성인 공개 패키지입니다. 이들은 고유한 UUID로 구분되며, 서로 다른 의존성을 가지고 있습니다:- 프라이빗
Priv
는Pub
및Zebra
패키지에 의존합니다. - 공용
Priv
는 의존성이 없습니다.
- 프라이빗
- 애플리케이션은 또한
Pub
패키지에 의존하며, 이는 다시 공개Priv
와 비공식Priv
패키지가 의존하는 동일한Zebra
패키지에 의존합니다.
이 의존성 그래프는 사전으로 표현되며, 다음과 같습니다:
graph = Dict(
# Priv – the private one:
UUID("ba13f791-ae1d-465a-978b-69c3ad90f72b") => Dict(
:Pub => UUID("c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1"),
:Zebra => UUID("f7a24cb4-21fc-4002-ac70-f0e3a0dd3f62"),
),
# Priv – the public one:
UUID("2d15fe94-a1f7-436c-a4d8-07a9a496e01c") => Dict(),
# Pub:
UUID("c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1") => Dict(
:Priv => UUID("2d15fe94-a1f7-436c-a4d8-07a9a496e01c"),
:Zebra => UUID("f7a24cb4-21fc-4002-ac70-f0e3a0dd3f62"),
),
# Zebra:
UUID("f7a24cb4-21fc-4002-ac70-f0e3a0dd3f62") => Dict(),
)
주어진 이 의존성 그래프
에서, Julia가 UUID c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1
인 Pub
패키지에서 import Priv
를 볼 때, 다음을 조회합니다:
graph[UUID("c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1")][:Priv]
그리고 2d15fe94-a1f7-436c-a4d8-07a9a496e01c
를 가져오면, Pub
패키지의 맥락에서 import Priv
가 앱이 직접 의존하는 개인 패키지가 아닌 공개 Priv
패키지를 참조한다는 것을 나타냅니다. 이렇게 하면 Priv
라는 이름이 메인 프로젝트의 다른 패키지와 그 패키지의 의존성 중 하나에서 다르게 참조될 수 있어 패키지 생태계에서 중복된 이름을 허용합니다.
import Zebra
가 메인 App
코드베이스에서 평가되면 어떻게 될까요? Zebra
가 프로젝트 파일에 나타나지 않기 때문에, 임포트는 실패할 것입니다. 비록 Zebra
가 매니페스트 파일에는 나타나지만 말입니다. 게다가, 만약 import Zebra
가 UUID 2d15fe94-a1f7-436c-a4d8-07a9a496e01c
를 가진 공개 Priv
패키지에서 발생한다면, 그 또한 실패할 것입니다. 왜냐하면 해당 Priv
패키지는 매니페스트 파일에 선언된 의존성이 없기 때문에 어떤 패키지도 로드할 수 없기 때문입니다. Zebra
패키지는 매니페스트 파일에 명시적 의존성으로 나타나는 패키지, 즉 Pub
패키지와 하나의 Priv
패키지에 의해서만 로드될 수 있습니다.
프로젝트 환경의 경로 맵은 매니페스트 파일에서 추출됩니다. 패키지 uuid
이름이 X
인 경로는 다음 규칙에 따라 결정됩니다(순서대로):
- 디렉토리의 프로젝트 파일이
uuid
와 이름X
와 일치하면, 다음 중 하나:- 최상위
path
항목이 있으면,uuid
는 해당 경로에 매핑되며, 프로젝트 파일이 포함된 디렉토리를 기준으로 해석됩니다. - 그렇지 않으면,
uuid
는 프로젝트 파일이 포함된 디렉토리를 기준으로src/X.jl
에 매핑됩니다.
- 최상위
- 위의 경우가 아니라면 프로젝트 파일에 해당하는 매니페스트 파일이 있고 매니페스트에
uuid
와 일치하는 구문이 포함되어 있다면:path
항목이 있는 경우, 해당 경로를 사용하세요 (매니페스트 파일이 포함된 디렉토리를 기준으로 상대 경로).git-tree-sha1
항목이 있는 경우,uuid
와git-tree-sha1
의 결정론적 해시 함수를 계산합니다—이를slug
라고 부릅니다—그리고 JuliaDEPOT_PATH
전역 배열의 각 디렉토리에서packages/X/$slug
라는 이름의 디렉토리를 찾습니다. 존재하는 첫 번째 디렉토리를 사용합니다.
이 중 어떤 결과가 성공하면, 소스 코드 진입점에 대한 경로는 해당 결과이거나 그 결과에서 src/X.jl
을 더한 상대 경로가 됩니다. 그렇지 않으면 uuid
에 대한 경로 매핑이 없습니다. X
를 로드할 때 소스 코드 경로가 발견되지 않으면 조회가 실패하고, 사용자는 적절한 패키지 버전을 설치하거나 다른 수정 조치를 취하라는 메시지를 받을 수 있습니다(예: X
를 의존성으로 선언하기).
위의 예제 매니페스트 파일에서 첫 번째 Priv
패키지의 경로—UUID가 ba13f791-ae1d-465a-978b-69c3ad90f72b
인 패키지—를 찾기 위해, Julia는 매니페스트 파일에서 해당 스탠자를 찾고, path
항목이 있음을 확인한 후, App
프로젝트 디렉토리에 상대적인 deps/Priv
를 살펴봅니다. App
코드가 /home/me/projects/App
에 있다고 가정하면, /home/me/projects/App/deps/Priv
가 존재하는지 확인하고, 따라서 그곳에서 Priv
를 로드합니다.
만약 반대로 줄리아가 UUID 2d15fe94-a1f7-436c-a4d8-07a9a496e01c
를 가진 다른 Priv
패키지를 로드하고 있다면, 매니페스트에서 해당 스탠자를 찾아 path
항목이 없고 git-tree-sha1
항목이 있는 것을 확인합니다. 그런 다음 이 UUID/SHA-1 쌍에 대한 slug
를 계산하는데, 이는 HDkrT
입니다(이 계산의 정확한 세부사항은 중요하지 않지만 일관되고 결정적입니다). 이는 이 Priv
패키지의 경로가 패키지 저장소 중 하나에서 packages/Priv/HDkrT/src/Priv.jl
이 될 것임을 의미합니다. DEPOT_PATH
의 내용이 ["/home/me/.julia", "/usr/local/julia"]
라고 가정하면, 줄리아는 다음 경로를 확인하여 존재하는지 확인합니다:
/home/me/.julia/packages/Priv/HDkrT
/usr/local/julia/packages/Priv/HDkrT
Julia는 발견된 저장소에서 packages/Priv/HDKrT/src/Priv.jl
파일의 공개 Priv
패키지를 로드하려고 할 때 이 중 존재하는 첫 번째 것을 사용합니다.
여기 위에서 제공된 Manifest에 따라 의존성 그래프를 위해 로컬 파일 시스템을 검색한 후, 예제 App
프로젝트 환경에 대한 가능한 경로 맵의 표현이 있습니다:
paths = Dict(
# Priv – the private one:
(UUID("ba13f791-ae1d-465a-978b-69c3ad90f72b"), :Priv) =>
# relative entry-point inside `App` repo:
"/home/me/projects/App/deps/Priv/src/Priv.jl",
# Priv – the public one:
(UUID("2d15fe94-a1f7-436c-a4d8-07a9a496e01c"), :Priv) =>
# package installed in the system depot:
"/usr/local/julia/packages/Priv/HDkr/src/Priv.jl",
# Pub:
(UUID("c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1"), :Pub) =>
# package installed in the user depot:
"/home/me/.julia/packages/Pub/oKpw/src/Pub.jl",
# Zebra:
(UUID("f7a24cb4-21fc-4002-ac70-f0e3a0dd3f62"), :Zebra) =>
# package installed in the system depot:
"/usr/local/julia/packages/Zebra/me9k/src/Zebra.jl",
)
이 예제 맵에는 세 가지 다른 종류의 패키지 위치가 포함되어 있습니다(첫 번째와 세 번째는 기본 로드 경로의 일부입니다):
- 프라이빗
Priv
패키지는 "vendored"로App
리포지토리 내에 있습니다. - 공용
Priv
및Zebra
패키지는 시스템 저장소에 있으며, 여기에는 시스템 관리자가 설치하고 관리하는 패키지가 있습니다. 이들은 시스템의 모든 사용자에게 제공됩니다. Pub
패키지는 사용자 저장소에 있으며, 여기에는 사용자가 설치한 패키지가 있습니다. 이러한 패키지는 설치한 사용자만 사용할 수 있습니다.
Package directories
패키지 디렉토리는 이름 충돌을 처리할 수 없는 더 간단한 종류의 환경을 제공합니다. 패키지 디렉토리에서 최상위 패키지의 집합은 "패키지처럼 보이는" 하위 디렉토리의 집합입니다. 패키지 디렉토리에 패키지 X
가 존재하려면 해당 디렉토리에 다음 "진입점" 파일 중 하나가 포함되어 있어야 합니다:
X.jl
X/src/X.jl
X.jl/src/X.jl
패키지 디렉토리에 있는 패키지가 가져올 수 있는 종속성은 패키지에 프로젝트 파일이 포함되어 있는지 여부에 따라 다릅니다:
- 프로젝트 파일이 있는 경우, 프로젝트 파일의
[deps]
섹션에 식별된 패키지만 가져올 수 있습니다. - 프로젝트 파일이 없으면 최상위 패키지를 임포트할 수 있습니다. 즉,
Main
또는 REPL에서 로드할 수 있는 동일한 패키지입니다.
루트 맵은 패키지 디렉토리의 내용을 검사하여 존재하는 모든 패키지의 목록을 생성함으로써 결정됩니다. 또한, 각 항목에는 다음과 같이 UUID가 할당됩니다: 폴더 X
내에서 발견된 특정 패키지에 대해...
X/Project.toml
파일이 존재하고uuid
항목이 있다면,uuid
는 해당 값입니다.X/Project.toml
이 존재하지만 최상위 UUID 항목이 없는 경우,uuid
는X/Project.toml
의 정규(실제) 경로를 해싱하여 생성된 더미 UUID입니다.- 그렇지 않으면(
Project.toml
이 존재하지 않는 경우),uuid
는 모든 값이 0인 nil UUID입니다.
프로젝트 디렉토리의 의존성 그래프는 각 패키지의 하위 디렉토리에 있는 프로젝트 파일의 존재와 내용에 의해 결정됩니다. 규칙은 다음과 같습니다:
- 패키지 하위 디렉토리에 프로젝트 파일이 없으면 그래프에서 생략되며, 해당 코드의 import 문은 메인 프로젝트 및 REPL과 동일하게 최상위로 처리됩니다.
- 패키지 하위 디렉토리에 프로젝트 파일이 있는 경우, 해당 UUID의 그래프 항목은 프로젝트 파일의
[deps]
맵이며, 이 섹션이 없으면 비어 있는 것으로 간주됩니다.
예를 들어, 패키지 디렉토리가 다음과 같은 구조와 내용을 가지고 있다고 가정해 보겠습니다:
Aardvark/
src/Aardvark.jl:
import Bobcat
import Cobra
Bobcat/
Project.toml:
[deps]
Cobra = "4725e24d-f727-424b-bca0-c4307a3456fa"
Dingo = "7a7925be-828c-4418-bbeb-bac8dfc843bc"
src/Bobcat.jl:
import Cobra
import Dingo
Cobra/
Project.toml:
uuid = "4725e24d-f727-424b-bca0-c4307a3456fa"
[deps]
Dingo = "7a7925be-828c-4418-bbeb-bac8dfc843bc"
src/Cobra.jl:
import Dingo
Dingo/
Project.toml:
uuid = "7a7925be-828c-4418-bbeb-bac8dfc843bc"
src/Dingo.jl:
# no imports
여기 사전으로 표현된 해당 루트 구조가 있습니다:
roots = Dict(
:Aardvark => UUID("00000000-0000-0000-0000-000000000000"), # no project file, nil UUID
:Bobcat => UUID("85ad11c7-31f6-5d08-84db-0a4914d4cadf"), # dummy UUID based on path
:Cobra => UUID("4725e24d-f727-424b-bca0-c4307a3456fa"), # UUID from project file
:Dingo => UUID("7a7925be-828c-4418-bbeb-bac8dfc843bc"), # UUID from project file
)
여기 그래프 구조에 해당하는 사전이 있습니다:
graph = Dict(
# Bobcat:
UUID("85ad11c7-31f6-5d08-84db-0a4914d4cadf") => Dict(
:Cobra => UUID("4725e24d-f727-424b-bca0-c4307a3456fa"),
:Dingo => UUID("7a7925be-828c-4418-bbeb-bac8dfc843bc"),
),
# Cobra:
UUID("4725e24d-f727-424b-bca0-c4307a3456fa") => Dict(
:Dingo => UUID("7a7925be-828c-4418-bbeb-bac8dfc843bc"),
),
# Dingo:
UUID("7a7925be-828c-4418-bbeb-bac8dfc843bc") => Dict(),
)
몇 가지 일반적인 규칙을 참고하세요:
- 프로젝트 파일이 없는 패키지는 모든 최상위 종속성에 의존할 수 있으며, 패키지 디렉토리의 모든 패키지가 최상위에서 사용 가능하므로 환경 내의 모든 패키지를 가져올 수 있습니다.
- 프로젝트 파일이 있는 패키지는 프로젝트 파일이 없는 패키지에 의존할 수 없습니다. 프로젝트 파일이 있는 패키지는
graph
에서 패키지를 로드할 수 있지만, 프로젝트 파일이 없는 패키지는graph
에 나타나지 않기 때문입니다. - 프로젝트 파일이 있지만 명시적인 UUID가 없는 패키지는 프로젝트 파일이 없는 패키지에 의해서만 의존될 수 있습니다. 이러한 패키지에 할당된 더미 UUID는 엄격히 내부적이기 때문입니다.
다음 예제에서 이러한 규칙의 특정 사례를 관찰하십시오:
Aardvark
는Bobcat
,Cobra
또는Dingo
중 어느 것이든 가져올 수 있습니다; 실제로Bobcat
과Cobra
를 가져옵니다.Bobcat
은Cobra
와Dingo
를 모두 가져올 수 있으며, 실제로 가져옵니다. 두 프로젝트 모두 UUID가 있는 프로젝트 파일을 가지고 있으며,Bobcat
의[deps]
섹션에 종속성으로 선언되어 있습니다.Bobcat
은Aardvark
에 의존할 수 없습니다. 왜냐하면Aardvark
에는 프로젝트 파일이 없기 때문입니다.코브라
는딩고
를 가져올 수 있으며 실제로 가져옵니다.딩고
는 프로젝트 파일과 UUID를 가지고 있으며,코브라
의[deps]
섹션에서 의존성으로 선언되어 있습니다.코브라
는아르드바크
나보브캣
에 의존할 수 없습니다. 왜냐하면 둘 다 실제 UUID가 없기 때문입니다.Dingo
는[deps]
섹션이 없는 프로젝트 파일 때문에 아무것도 가져올 수 없습니다.
패스 맵은 패키지 디렉토리에서 간단합니다: 서브디렉토리 이름을 해당 엔트리 포인트 경로에 매핑합니다. 다시 말해, 예제 프로젝트 디렉토리의 경로가 /home/me/animals
라면 paths
맵은 다음과 같은 사전으로 표현될 수 있습니다:
paths = Dict(
(UUID("00000000-0000-0000-0000-000000000000"), :Aardvark) =>
"/home/me/AnimalPackages/Aardvark/src/Aardvark.jl",
(UUID("85ad11c7-31f6-5d08-84db-0a4914d4cadf"), :Bobcat) =>
"/home/me/AnimalPackages/Bobcat/src/Bobcat.jl",
(UUID("4725e24d-f727-424b-bca0-c4307a3456fa"), :Cobra) =>
"/home/me/AnimalPackages/Cobra/src/Cobra.jl",
(UUID("7a7925be-828c-4418-bbeb-bac8dfc843bc"), :Dingo) =>
"/home/me/AnimalPackages/Dingo/src/Dingo.jl",
)
패키지 디렉토리 환경의 모든 패키지는 정의상 예상되는 진입점 파일을 가진 하위 디렉토리이므로, 이들의 paths
맵 항목은 항상 이 형식을 갖습니다.
Environment stacks
세 번째이자 마지막 종류의 환경은 여러 환경을 겹쳐서 결합하여 각 환경의 패키지를 단일 복합 환경에서 사용할 수 있도록 하는 것입니다. 이러한 복합 환경을 환경 스택이라고 합니다. Julia의 LOAD_PATH
전역은 환경 스택을 정의하며, Julia 프로세스가 작동하는 환경입니다. Julia 프로세스가 하나의 프로젝트나 패키지 디렉토리에 있는 패키지에만 접근하도록 하려면, 그것을 LOAD_PATH
의 유일한 항목으로 설정하십시오. 그러나 작업 중인 프로젝트의 종속성이 아니더라도 좋아하는 도구—표준 라이브러리, 프로파일러, 디버거, 개인 유틸리티 등—에 접근할 수 있는 것이 종종 매우 유용합니다. 이러한 도구가 포함된 환경을 로드 경로에 추가하면, 프로젝트에 추가할 필요 없이 최상위 코드에서 즉시 사용할 수 있습니다.
환경 스택의 구성 요소인 루트, 그래프 및 경로 데이터 구조를 결합하는 메커니즘은 간단합니다: 키 충돌의 경우 나중의 항목보다 이전 항목을 우선시하여 사전으로 병합됩니다. 다시 말해, stack = [env₁, env₂, …]
가 있다면:
roots = reduce(merge, reverse([roots₁, roots₂, …]))
graph = reduce(merge, reverse([graph₁, graph₂, …]))
paths = reduce(merge, reverse([paths₁, paths₂, …]))
서브스크립트가 있는 rootsᵢ
, graphᵢ
및 pathsᵢ
변수는 stack
에 포함된 서브스크립트 환경 envᵢ
에 해당합니다. reverse
는 merge
가 인수 사전의 키 간 충돌이 있을 때 첫 번째가 아닌 마지막 인수를 선호하기 때문에 존재합니다. 이 설계의 몇 가지 주목할 만한 특징이 있습니다:
- 기본 환경—즉, 스택에서 첫 번째 환경—은 스택된 환경에 충실하게 포함됩니다. 스택에서 첫 번째 환경의 전체 의존성 그래프는 모든 의존성의 동일한 버전을 포함하여 스택된 환경에 온전하게 포함될 것이라고 보장됩니다.
- 비주요 환경의 패키지는 자신의 환경이 완전히 호환 가능하더라도 종속성의 호환되지 않는 버전을 사용할 수 있습니다. 이는 종속성 중 하나가 스택의 이전 환경에 있는 버전에 의해 가려질 때 발생할 수 있습니다(그래프 또는 경로, 또는 둘 다에 의해).
주 환경은 일반적으로 작업 중인 프로젝트의 환경이기 때문에, 스택의 후속 환경에는 추가 도구가 포함되어 있습니다. 따라서 이는 올바른 균형입니다: 개발 도구를 깨뜨리는 것보다 프로젝트가 작동하는 것이 더 좋습니다. 이러한 비호환성이 발생할 때, 일반적으로 주요 프로젝트와 호환되는 버전으로 개발 도구를 업그레이드하고 싶을 것입니다.
Package Extensions
패키지 "extension"은 현재 Julia 세션에서 지정된 다른 패키지 세트(그의 "트리거")가 로드될 때 자동으로 로드되는 모듈입니다. 확장은 프로젝트 파일의 [extensions]
섹션 아래에 정의됩니다. 확장의 트리거는 프로젝트 파일의 [weakdeps]
섹션(그리고 가능하지만 드물게 [deps]
섹션) 아래에 나열된 패키지의 하위 집합입니다. 이러한 패키지는 다른 패키지와 마찬가지로 호환성 항목을 가질 수 있습니다.
name = "MyPackage"
[compat]
ExtDep = "1.0"
OtherExtDep = "1.0"
[weakdeps]
ExtDep = "c9a23..." # uuid
OtherExtDep = "862e..." # uuid
[extensions]
BarExt = ["ExtDep", "OtherExtDep"]
FooExt = "ExtDep"
...
extensions
아래의 키는 확장 프로그램의 이름입니다. 이들은 해당 확장의 오른쪽에 있는 모든 패키지(트리거)가 로드될 때 로드됩니다. 확장 프로그램에 트리거가 하나만 있는 경우, 트리거 목록은 간결성을 위해 문자열로 작성할 수 있습니다. 확장의 진입점 위치는 ext/FooExt.jl
또는 ext/FooExt/FooExt.jl
입니다(확장 프로그램 FooExt
의 경우). 확장의 내용은 종종 다음과 같이 구조화됩니다:
module FooExt
# Load main package and triggers
using MyPackage, ExtDep
# Extend functionality in main package with types from the triggers
MyPackage.func(x::ExtDep.SomeStruct) = ...
end
패키지에 확장 기능이 추가되면, weakdeps
및 extensions
섹션이 해당 패키지의 매니페스트 파일에 저장됩니다. 패키지에 대한 의존성 조회 규칙은 "부모"와 동일하지만, 나열된 트리거도 의존성으로 간주됩니다.
Package/Environment Preferences
Preferences는 환경 내에서 패키지 동작에 영향을 미치는 메타데이터의 사전입니다. Preferences 시스템은 컴파일 시간에 Preferences를 읽는 것을 지원하므로, 코드 로딩 시간에 Julia가 선택한 사전 컴파일 파일이 현재 환경과 동일한 Preferences로 빌드되었는지 확인해야 합니다. Preferences를 수정하기 위한 공개 API는 Preferences.jl 패키지에 포함되어 있습니다. Preferences는 현재 활성 프로젝트 옆에 있는 (Julia)LocalPreferences.toml
파일 내에 TOML 사전으로 저장됩니다. 만약 Preference가 "내보내기"되면, 대신 (Julia)Project.toml
내에 저장됩니다. 의도는 공유 프로젝트가 공유 Preferences를 포함할 수 있도록 하면서, 사용자가 자신의 설정으로 이러한 Preferences를 LocalPreferences.toml 파일에서 재정의할 수 있도록 하는 것입니다. 이 파일은 이름이 암시하듯이 .gitignored 되어야 합니다.
컴파일 중에 접근되는 설정은 자동으로 컴파일 시간 설정으로 표시되며, 이러한 설정에 기록된 변경 사항은 Julia 컴파일러가 해당 모듈에 대한 모든 캐시된 사전 컴파일 파일(.ji
및 해당 .so
, .dll
또는 .dylib
파일)을 다시 컴파일하도록 합니다. 이는 컴파일 중에 모든 컴파일 시간 설정의 해시를 직렬화한 다음, 적절한 파일을 로드할 때 현재 환경과 해당 해시를 비교하여 수행됩니다.
Preferences는 저장소 전체 기본값으로 설정할 수 있습니다. 패키지 Foo가 전역 환경에 설치되어 있고 설정된 기본값이 있는 경우, 이러한 기본값은 전역 환경이 LOAD_PATH
의 일부인 한 적용됩니다. 환경 스택에서 더 높은 환경의 기본값은 로드 경로의 더 가까운 항목에 의해 덮어씌워지며, 현재 활성 프로젝트로 끝납니다. 이를 통해 저장소 전체 기본값이 존재할 수 있으며, 활성 프로젝트는 이러한 상속된 기본값을 병합하거나 완전히 덮어쓸 수 있습니다. 병합을 허용하거나 허용하지 않도록 기본값을 설정하는 방법에 대한 전체 세부정보는 Preferences.set_preferences!()
의 문서 문자열을 참조하십시오.
Conclusion
연합 패키지 관리와 정밀한 소프트웨어 재현성은 패키지 시스템에서 어렵지만 가치 있는 목표입니다. 이 목표들이 결합되면 대부분의 동적 언어가 갖고 있는 것보다 더 복잡한 패키지 로딩 메커니즘이 생기지만, 이는 정적 언어와 더 일반적으로 연관된 확장성과 재현성을 제공합니다. 일반적으로, 줄리아 사용자는 이러한 상호작용에 대한 정확한 이해 없이도 내장된 패키지 관리자를 사용하여 프로젝트를 관리할 수 있어야 합니다. Pkg.add("X")
를 호출하면 Pkg.activate("Y")
를 통해 선택된 적절한 프로젝트 및 매니페스트 파일에 추가되므로, 이후 import X
를 호출하면 추가적인 생각 없이 X
가 로드됩니다.