Code Loading

Note

이 장에서는 패키지 로딩의 기술적 세부 사항을 다룹니다. 패키지를 설치하려면 Pkg를 사용하여 Julia의 내장 패키지 관리자를 통해 활성 환경에 패키지를 추가하세요. 활성 환경에 이미 있는 패키지를 사용하려면 import X 또는 using X를 작성하면 됩니다. 이는 Modules documentation에 설명되어 있습니다.

Definitions

줄리아는 코드를 로드하는 두 가지 메커니즘을 가지고 있습니다:

  1. 코드 포함: 예: include("source.jl"). 포함은 단일 프로그램을 여러 소스 파일로 나눌 수 있게 해줍니다. 표현식 include("source.jl")include 호출이 발생하는 모듈의 전역 범위에서 source.jl 파일의 내용을 평가하게 합니다. include("source.jl")가 여러 번 호출되면, source.jl은 여러 번 평가됩니다. 포함된 경로인 source.jlinclude 호출이 발생하는 파일을 기준으로 상대적으로 해석됩니다. 이는 소스 파일의 하위 트리를 쉽게 이동할 수 있게 해줍니다. REPL에서는 포함된 경로가 현재 작업 디렉토리를 기준으로 해석됩니다, pwd().
  2. 패키지 로딩: 예: 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이라는 애플리케이션을 개발하고 있다고 가정해 보겠습니다. 이 애플리케이션은 두 개의 패키지인 PubPriv를 사용합니다. 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 Xusing X가 의미하는 바와 이러한 문장이 어떤 파일을 로드하게 되는지를 결정합니다. Julia는 두 가지 종류의 환경을 이해합니다:

  1. 프로젝트 환경은 프로젝트 파일과 선택적 매니페스트 파일이 있는 디렉토리이며, 명시적 환경을 형성합니다. 프로젝트 파일은 프로젝트의 직접 종속성의 이름과 정체성을 결정합니다. 매니페스트 파일이 있는 경우, 모든 직접 및 간접 종속성을 포함한 완전한 종속성 그래프, 각 종속성의 정확한 버전, 그리고 올바른 버전을 찾고 로드하는 데 필요한 충분한 정보를 제공합니다.
  2. 패키지 디렉토리는 하위 디렉토리로서 일련의 패키지의 소스 트리를 포함하는 디렉토리이며, 암묵적 환경을 형성합니다. 만약 X가 패키지 디렉토리의 하위 디렉토리이고 X/src/X.jl이 존재한다면, 패키지 디렉토리 환경에서 패키지 X를 사용할 수 있으며 X/src/X.jl은 그것이 로드되는 소스 파일입니다.

이들은 스택 환경을 생성하기 위해 혼합될 수 있습니다: 프로젝트 환경과 패키지 디렉토리의 정렬된 집합이 겹쳐져 단일 복합 환경을 만듭니다. 그런 다음 우선 순위 및 가시성 규칙이 결합되어 어떤 패키지가 사용 가능한지와 어디에서 로드되는지를 결정합니다. 예를 들어, 줄리아의 로드 경로는 스택 환경을 형성합니다.

이러한 환경은 각각 다른 목적을 가지고 있습니다:

  • 프로젝트 환경은 재현성을 제공합니다. 프로젝트 환경을 버전 관리에 체크인함으로써—예: git 저장소—프로젝트의 소스 코드와 함께 프로젝트의 정확한 상태와 모든 종속성을 재현할 수 있습니다. 특히 매니페스트 파일은 모든 종속성의 정확한 버전을 캡처하며, 이는 소스 트리의 암호화 해시로 식별되어 Pkg가 올바른 버전을 검색하고 모든 종속성에 대해 기록된 정확한 코드를 실행하고 있는지 확신할 수 있게 합니다.
  • 패키지 디렉토리는 전체적으로 신중하게 추적된 프로젝트 환경이 필요하지 않을 때 편리함을 제공합니다. 패키지 세트를 어딘가에 두고 프로젝트 환경을 만들 필요 없이 직접 사용할 수 있을 때 유용합니다.
  • 스택 환경은 도구를 기본 환경에 추가할 수 있게 해줍니다. 개발 도구의 환경을 스택의 끝에 푸시하여 REPL 및 스크립트에서 사용할 수 있도록 할 수 있지만, 패키지 내부에서는 사용할 수 없습니다.

높은 수준에서 각 환경은 개념적으로 세 가지 맵을 정의합니다: 루트, 그래프 및 경로. import X의 의미를 해석할 때, 루트와 그래프 맵은 X의 정체성을 결정하는 데 사용되며, 경로 맵은 X의 소스 코드를 찾는 데 사용됩니다. 세 가지 맵의 구체적인 역할은 다음과 같습니다:

  • 루트: name::Symboluuid::UUID

    환경의 루트 맵은 환경이 메인 프로젝트에 제공하는 모든 최상위 종속성에 대해 패키지 이름을 UUID에 할당합니다(즉, Main에서 로드할 수 있는 것들). 줄리아가 메인 프로젝트에서 import X를 만났을 때, X의 정체성을 roots[:X]로 조회합니다.

  • 그래프: context::UUIDname::Symboluuid::UUID

    환경의 그래프는 각 context UUID에 대해 이름에서 UUID로의 맵을 할당하는 다단계 맵입니다. 이는 루트 맵과 유사하지만 해당 context에 특정적입니다. Julia가 UUID가 context인 패키지의 코드에서 import X를 볼 때, X의 정체성을 graph[context][:X]로 조회합니다. 특히, 이는 import Xcontext에 따라 서로 다른 패키지를 참조할 수 있음을 의미합니다.

  • 경로: uuid::UUID × name::Symbolpath::String

    경로 맵은 각 패키지 UUID-이름 쌍에 대해 해당 패키지의 진입점 소스 파일의 위치를 할당합니다. import X에서 X의 정체성이 루트 또는 그래프를 통해 UUID로 확인된 후(주 프로젝트에서 로드되었는지 또는 종속성에서 로드되었는지에 따라), Julia는 환경에서 paths[uuid,:X]를 조회하여 X를 가져오기 위해 로드할 파일을 결정합니다. 이 파일을 포함하면 X라는 이름의 모듈이 정의되어야 합니다. 이 패키지가 로드되면, 동일한 uuid로 해결되는 이후의 모든 import는 이미 로드된 패키지 모듈에 대한 새로운 바인딩을 생성합니다.

각 종류의 환경은 다음 섹션에 자세히 설명된 대로 이 세 가지 맵을 다르게 정의합니다.

Note

이 장의 예제는 루트, 그래프 및 경로에 대한 전체 데이터 구조를 보여줍니다. 그러나 줄리아의 패키지 로딩 코드는 이를 명시적으로 생성하지 않습니다. 대신, 필요한 패키지를 로드하기 위해 각 구조체의 필요한 부분만 지연 계산합니다.

Project environments

프로젝트 환경은 Project.toml이라는 프로젝트 파일을 포함하는 디렉토리에 의해 결정되며, 선택적으로 Manifest.toml이라는 매니페스트 파일을 포함할 수 있습니다. 이러한 파일은 JuliaProject.tomlJuliaManifest.toml이라고도 불릴 수 있으며, 이 경우 Project.tomlManifest.toml은 무시됩니다. 이는 Project.tomlManifest.toml이라는 파일을 중요하게 여기는 다른 도구와의 공존을 허용합니다. 그러나 순수 Julia 프로젝트의 경우, Project.tomlManifest.toml이라는 이름이 선호됩니다. 그러나 Julia v1.10.8 이후부터는 (Julia)Manifest-v{major}.{minor}.toml 형식이 인식되어 특정 매니페스트 파일을 사용하도록 특정 Julia 버전을 설정할 수 있습니다. 즉, 동일한 폴더 내에서 Manifest-v1.11.toml은 v1.11에 의해 사용되고, Manifest.toml은 다른 모든 Julia 버전에 의해 사용됩니다.

프로젝트 환경의 루트, 그래프 및 경로 맵은 다음과 같이 정의됩니다:

환경의 루트 맵은 프로젝트 파일의 내용에 의해 결정되며, 특히 최상위 nameuuid 항목과 [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로 구분되며, 서로 다른 의존성을 가지고 있습니다:
    • 프라이빗 PrivPubZebra 패키지에 의존합니다.
    • 공용 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-fadaaeaf08e1Pub 패키지에서 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인 경로는 다음 규칙에 따라 결정됩니다(순서대로):

  1. 디렉토리의 프로젝트 파일이 uuid와 이름 X와 일치하면, 다음 중 하나:
    • 최상위 path 항목이 있으면, uuid는 해당 경로에 매핑되며, 프로젝트 파일이 포함된 디렉토리를 기준으로 해석됩니다.
    • 그렇지 않으면, uuid는 프로젝트 파일이 포함된 디렉토리를 기준으로 src/X.jl에 매핑됩니다.
  2. 위의 경우가 아니라면 프로젝트 파일에 해당하는 매니페스트 파일이 있고 매니페스트에 uuid와 일치하는 구문이 포함되어 있다면:
    • path 항목이 있는 경우, 해당 경로를 사용하세요 (매니페스트 파일이 포함된 디렉토리를 기준으로 상대 경로).
    • git-tree-sha1 항목이 있는 경우, uuidgit-tree-sha1의 결정론적 해시 함수를 계산합니다—이를 slug라고 부릅니다—그리고 Julia DEPOT_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"]라고 가정하면, 줄리아는 다음 경로를 확인하여 존재하는지 확인합니다:

  1. /home/me/.julia/packages/Priv/HDkrT
  2. /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",
)

이 예제 맵에는 세 가지 다른 종류의 패키지 위치가 포함되어 있습니다(첫 번째와 세 번째는 기본 로드 경로의 일부입니다):

  1. 프라이빗 Priv 패키지는 "vendored"로 App 리포지토리 내에 있습니다.
  2. 공용 PrivZebra 패키지는 시스템 저장소에 있으며, 여기에는 시스템 관리자가 설치하고 관리하는 패키지가 있습니다. 이들은 시스템의 모든 사용자에게 제공됩니다.
  3. Pub 패키지는 사용자 저장소에 있으며, 여기에는 사용자가 설치한 패키지가 있습니다. 이러한 패키지는 설치한 사용자만 사용할 수 있습니다.

Package directories

패키지 디렉토리는 이름 충돌을 처리할 수 없는 더 간단한 종류의 환경을 제공합니다. 패키지 디렉토리에서 최상위 패키지의 집합은 "패키지처럼 보이는" 하위 디렉토리의 집합입니다. 패키지 디렉토리에 패키지 X가 존재하려면 해당 디렉토리에 다음 "진입점" 파일 중 하나가 포함되어 있어야 합니다:

  • X.jl
  • X/src/X.jl
  • X.jl/src/X.jl

패키지 디렉토리에 있는 패키지가 가져올 수 있는 종속성은 패키지에 프로젝트 파일이 포함되어 있는지 여부에 따라 다릅니다:

  • 프로젝트 파일이 있는 경우, 프로젝트 파일의 [deps] 섹션에 식별된 패키지만 가져올 수 있습니다.
  • 프로젝트 파일이 없으면 최상위 패키지를 임포트할 수 있습니다. 즉, Main 또는 REPL에서 로드할 수 있는 동일한 패키지입니다.

루트 맵은 패키지 디렉토리의 내용을 검사하여 존재하는 모든 패키지의 목록을 생성함으로써 결정됩니다. 또한, 각 항목에는 다음과 같이 UUID가 할당됩니다: 폴더 X 내에서 발견된 특정 패키지에 대해...

  1. X/Project.toml 파일이 존재하고 uuid 항목이 있다면, uuid는 해당 값입니다.
  2. X/Project.toml이 존재하지만 최상위 UUID 항목이 없는 경우, uuidX/Project.toml의 정규(실제) 경로를 해싱하여 생성된 더미 UUID입니다.
  3. 그렇지 않으면(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(),
)

몇 가지 일반적인 규칙을 참고하세요:

  1. 프로젝트 파일이 없는 패키지는 모든 최상위 종속성에 의존할 수 있으며, 패키지 디렉토리의 모든 패키지가 최상위에서 사용 가능하므로 환경 내의 모든 패키지를 가져올 수 있습니다.
  2. 프로젝트 파일이 있는 패키지는 프로젝트 파일이 없는 패키지에 의존할 수 없습니다. 프로젝트 파일이 있는 패키지는 graph에서 패키지를 로드할 수 있지만, 프로젝트 파일이 없는 패키지는 graph에 나타나지 않기 때문입니다.
  3. 프로젝트 파일이 있지만 명시적인 UUID가 없는 패키지는 프로젝트 파일이 없는 패키지에 의해서만 의존될 수 있습니다. 이러한 패키지에 할당된 더미 UUID는 엄격히 내부적이기 때문입니다.

다음 예제에서 이러한 규칙의 특정 사례를 관찰하십시오:

  • AardvarkBobcat, Cobra 또는 Dingo 중 어느 것이든 가져올 수 있습니다; 실제로 BobcatCobra를 가져옵니다.
  • BobcatCobraDingo를 모두 가져올 수 있으며, 실제로 가져옵니다. 두 프로젝트 모두 UUID가 있는 프로젝트 파일을 가지고 있으며, Bobcat[deps] 섹션에 종속성으로 선언되어 있습니다.
  • BobcatAardvark에 의존할 수 없습니다. 왜냐하면 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ᵢ에 해당합니다. reversemerge가 인수 사전의 키 간 충돌이 있을 때 첫 번째가 아닌 마지막 인수를 선호하기 때문에 존재합니다. 이 설계의 몇 가지 주목할 만한 특징이 있습니다:

  1. 기본 환경—즉, 스택에서 첫 번째 환경—은 스택된 환경에 충실하게 포함됩니다. 스택에서 첫 번째 환경의 전체 의존성 그래프는 모든 의존성의 동일한 버전을 포함하여 스택된 환경에 온전하게 포함될 것이라고 보장됩니다.
  2. 비주요 환경의 패키지는 자신의 환경이 완전히 호환 가능하더라도 종속성의 호환되지 않는 버전을 사용할 수 있습니다. 이는 종속성 중 하나가 스택의 이전 환경에 있는 버전에 의해 가려질 때 발생할 수 있습니다(그래프 또는 경로, 또는 둘 다에 의해).

주 환경은 일반적으로 작업 중인 프로젝트의 환경이기 때문에, 스택의 후속 환경에는 추가 도구가 포함되어 있습니다. 따라서 이는 올바른 균형입니다: 개발 도구를 깨뜨리는 것보다 프로젝트가 작동하는 것이 더 좋습니다. 이러한 비호환성이 발생할 때, 일반적으로 주요 프로젝트와 호환되는 버전으로 개발 도구를 업그레이드하고 싶을 것입니다.

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

패키지에 확장 기능이 추가되면, weakdepsextensions 섹션이 해당 패키지의 매니페스트 파일에 저장됩니다. 패키지에 대한 의존성 조회 규칙은 "부모"와 동일하지만, 나열된 트리거도 의존성으로 간주됩니다.

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가 로드됩니다.