代码加载

注意

本章介绍了包加载的技术细节。要安装包,请使用 Pkg(Julia 内置的包管理器)将包添加到您的活动环境中。要使用已在您的活动环境中的包,请编写 import Xusing X,如 模块文档 中所述。

定义

Julia 有两种加载代码的机制

  1. 代码包含:例如 include("source.jl")。包含允许您将单个程序拆分为多个源文件。表达式 include("source.jl") 会导致文件 source.jl 的内容在发生 include 调用的模块的全局作用域中进行评估。如果 include("source.jl") 被多次调用,则 source.jl 会被多次评估。包含的路径 source.jl 是相对于发生 include 调用的文件进行解释的。这使得重新定位源文件的子树变得简单。在 REPL 中,包含的路径是相对于当前工作目录进行解释的,pwd()
  2. 包加载:例如 import Xusing X。导入机制允许您加载一个包(即一个独立的、可重用的 Julia 代码集合,封装在一个模块中),并在导入模块内部通过名称 X 使结果模块可用。如果在同一个 Julia 会话中多次导入同一个 X 包,则它只在第一次加载时加载——在后续导入中,导入模块会获得对同一个模块的引用。但请注意,import X 可以在不同的上下文中加载不同的包:X 可以指代主项目中名为 X 的一个包,但在每个依赖项中也可能指代名为 X 的不同包。更多信息请见下文。

代码包含非常简单明了:它在调用方的上下文中评估给定的源文件。包加载建立在代码包含的基础上,并服务于 不同的目的。本章的其余部分重点介绍包加载的行为和机制。

是一个具有标准布局的源代码树,它提供可供其他 Julia 项目重用的功能。包通过 import Xusing X 语句加载。这些语句还使名为 X 的模块(加载包代码后产生的)在发生导入语句的模块中可用。import XX 的含义取决于上下文:加载哪个 X 包取决于语句出现在哪个代码中。因此,import X 的处理分为两个阶段:首先,它确定哪个包在此上下文中被定义为 X;其次,它确定该特定 X在哪里找到。

这些问题可以通过在 LOAD_PATH 中列出的项目环境中搜索项目文件 (Project.tomlJuliaProject.toml)、清单文件 (Manifest.tomlJuliaManifest.toml) 或源文件文件夹来解答。

包的联合

大多数情况下,只需根据包的名称即可唯一识别它。但是,有时项目可能会遇到需要使用两个具有相同名称的不同包的情况。虽然您可以通过重命名其中一个包来解决此问题,但这在大型共享代码库中可能会造成很大的干扰。相反,Julia 的代码加载机制允许同一个包名称在应用程序的不同组件中引用不同的包。

Julia 支持联合包管理,这意味着多个独立方都可以维护公共和私有包以及包注册表,并且项目可以依赖于来自不同注册表的公共和私有包的混合。来自各个注册表的包可以使用一组通用的工具和工作流程进行安装和管理。与 Julia 一起提供的 Pkg 包管理器允许您安装和管理项目的依赖项。它有助于创建和操作项目文件(描述您的项目依赖的其他项目)和清单文件(捕捉项目完整依赖关系图的精确版本),以及足够的信息以找到和加载正确的版本。

联合的一个结果是,不可能存在包命名的中央权威机构。不同的实体可以使用相同的名称来指代不相关的包。这种可能性是不可避免的,因为这些实体不协调,甚至可能彼此不知道。由于缺乏中央命名权威,单个项目最终可能会依赖于具有相同名称的不同包。Julia 的包加载机制不要求包名称在全局范围内唯一,即使在单个项目的依赖关系图内也是如此。相反,包由 通用唯一标识符 (UUID) 标识,这些标识符在创建每个包时都会分配。通常您不必直接使用这些有点麻烦的 128 位标识符,因为 Pkg 会为您处理生成和跟踪它们。但是,这些 UUID 为“X 指代哪个包?”的问题提供了明确的答案。

由于去中心化的命名问题有点抽象,因此逐步了解一个具体场景可能有助于理解这个问题。假设您正在开发一个名为 App 的应用程序,它使用两个包:PubPrivPriv 是您创建的私有包,而 Pub 是您使用但无法控制的公共包。当您创建 Priv 时,没有名为 Priv 的公共包。但是,随后,一个也名为 Priv 的不相关的包已经被发布并变得流行起来。事实上,Pub 包已经开始使用它。因此,当您接下来升级 Pub 以获取最新的错误修复和功能时,App 最终会依赖于两个名为 Priv 的不同包——除了升级之外,您没有采取任何其他操作。App 直接依赖于您的私有 Priv 包,并通过 Pub 间接依赖于新的公共 Priv 包。由于这两个 Priv 包是不同的,但 App 为了继续正常工作都需要它们,因此表达式 import Priv 必须引用不同的 Priv 包,具体取决于它是在 App 的代码中还是在 Pub 的代码中。为了处理这种情况,Julia 的包加载机制通过它们的 UUID 区分这两个 Priv 包,并根据其上下文(调用 import 的模块)选择正确的包。这种区分的工作方式由环境决定,如下面的部分所述。

环境

环境确定在各种代码上下文中 import Xusing X 的含义,以及这些语句导致加载哪些文件。Julia 理解两种类型的环境

  1. 项目环境是一个带有项目文件和可选清单文件的目录,并形成一个显式环境。项目文件确定项目的直接依赖项的名称和标识是什么。如果存在,清单文件会提供完整的依赖关系图,包括所有直接和间接依赖项、每个依赖项的确切版本,以及查找和加载正确版本的信息。
  2. 包目录是一个包含一组包的源代码树作为子目录的目录,并形成一个隐式环境。如果 X 是包目录的子目录,并且存在 X/src/X.jl,则包 X 在包目录环境中可用,并且 X/src/X.jl 是通过它加载的源文件。

这些环境可以混合使用来创建一个**叠加环境**:一个项目的环境和包目录的有序集合,叠加在一起形成一个单一的复合环境。然后,优先级和可见性规则结合起来确定哪些包可用以及从哪里加载它们。例如,Julia 的加载路径就形成了一个叠加环境。

每个环境都服务于不同的目的。

  • 项目环境提供**可重复性**。通过将项目环境与项目的其余源代码一起检入版本控制(例如,git 仓库),您可以重现项目的精确状态及其所有依赖项。特别是,清单文件捕获了每个依赖项的确切版本,通过其源代码树的加密哈希进行标识,这使得 Pkg 可以检索正确的版本并确保您正在运行为所有依赖项记录的确切代码。
  • 当不需要完整的、经过仔细跟踪的项目环境时,包目录提供**便利性**。当您想将一组包放在某个地方并能够直接使用它们时,它们很有用,而无需为它们创建项目环境。
  • 叠加环境允许向主环境**添加**工具。您可以将开发工具的环境推送到堆栈的末尾,以便从 REPL 和脚本中使用它们,但不能从包内部使用。

在高级别上,每个环境从概念上定义了三个映射:根、图和路径。在解析 import X 的含义时,根和图映射用于确定 X 的标识,而路径映射用于定位 X 的源代码。三个映射的具体作用是

  • **根:** name::Symboluuid::UUID

    环境的根映射将包名称分配给环境提供给主项目的所有顶级依赖项的 UUID(即可以在 Main 中加载的依赖项)。当 Julia 在主项目中遇到 import X 时,它会查找 X 的标识,即 roots[:X]

  • **图:** context::UUIDname::Symboluuid::UUID

    环境的图是一个多级映射,它为每个 context UUID 分配一个从名称到 UUID 的映射,类似于根映射,但特定于该 context。当 Julia 在 UUID 为 context 的包的代码中看到 import X 时,它会查找 X 的标识,即 graph[context][:X]。特别是,这意味着 import X 可以根据 context 指向不同的包。

  • **路径:** uuid::UUID × name::Symbolpath::String

    路径映射将每个包 UUID-名称对分配到该包的入口点源文件的位置。在 import XX 的标识通过根或图(取决于它是从主项目还是依赖项加载)解析为 UUID 后,Julia 通过在环境中查找 paths[uuid,:X] 来确定要加载哪个文件以获取 X。包含此文件应该定义一个名为 X 的模块。加载此包后,任何随后解析为相同 uuid 的导入都将创建一个指向已加载包模块的新绑定。

每种类型的环境都以不同的方式定义这三个映射,详见以下部分。

注意

为了便于理解,本章中的示例显示了根、图和路径的完整数据结构。但是,Julia 的包加载代码不会显式创建这些结构。相反,它只根据需要延迟计算每个结构的一部分以加载给定的包。

项目环境

项目环境由包含名为 Project.toml 的项目文件和可选的名为 Manifest.toml 的清单文件来确定。这些文件也可以称为 JuliaProject.tomlJuliaManifest.toml,在这种情况下,Project.tomlManifest.toml 将被忽略。这允许与可能认为名为 Project.tomlManifest.toml 的文件很重要的其他工具共存。但是,对于纯 Julia 项目,首选名称 Project.tomlManifest.toml

项目环境的根、图和路径映射定义如下

**根映射**由项目文件的内容确定,具体来说,是其顶级 nameuuid 条目及其 [deps] 部分(所有这些都是可选的)。考虑以下假设应用程序 App 的示例项目文件,如前所述

name = "App"
uuid = "8f986787-14fe-4607-ba5d-fbff2944afa9"

[deps]
Priv = "ba13f791-ae1d-465a-978b-69c3ad90f72b"
Pub  = "c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1"

如果此项目文件由 Julia 字典表示,则它表示以下根映射

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],这将产生 ba13f791-ae1d-465a-978b-69c3ad90f72b,即在该上下文中要加载的 Priv 包的 UUID。此 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 依赖于 PubZebra 包。
    • 公共 Priv 没有依赖项。
  • 应用程序还依赖于 Pub 包,而 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(),
)

给定此依赖项 graph,当 Julia 在 Pub 包(其 UUID 为 c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1)中看到 import Priv 时,它会查找

graph[UUID("c07ecb7d-0dc9-4db7-8803-fadaaeaf08e1")][:Priv]

并获得 2d15fe94-a1f7-436c-a4d8-07a9a496e01c,这表明在 Pub 包的上下文中,import Priv 指的是公共 Priv 包,而不是应用程序直接依赖的私有包。这就是名称 Priv 如何能够在主项目中引用与在其中一个包的依赖项中不同的包,这允许在包生态系统中使用重复的名称。

如果在主 App 代码库中评估 import Zebra 会发生什么?由于 Zebra 未出现在项目文件中,因此即使 Zebra 出现在清单文件中,导入也会失败。此外,如果 import Zebra 出现在公共 Priv 包(UUID 为 2d15fe94-a1f7-436c-a4d8-07a9a496e01c 的那个)中,那么也会失败,因为该 Priv 包在清单文件中没有声明的依赖项,因此无法加载任何包。Zebra 包只能由将其作为清单文件中显式依赖项的包加载:Pub 包和其中一个 Priv 包。

**路径映射**是从清单文件中提取的。名为 X 的包 uuid 的路径由以下规则确定(按顺序)

  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

另一方面,如果 Julia 正在加载另一个 Priv 包(UUID 为 2d15fe94-a1f7-436c-a4d8-07a9a496e01c),它会在清单文件中找到其节,发现它没有 path 条目,但它确实具有 git-tree-sha1 条目。然后,它会计算此 UUID/SHA-1 对的 slug,即 HDkrT(此计算的确切细节并不重要,但它是一致且确定的)。这意味着此 Priv 包的路径将在包仓库中的某个目录中为 packages/Priv/HDkrT/src/Priv.jl。假设 DEPOT_PATH 的内容为 ["/home/me/.julia", "/usr/local/julia"],则 Julia 将查看以下路径以查看它们是否存在

  1. /home/me/.julia/packages/Priv/HDkrT
  2. /usr/local/julia/packages/Priv/HDkrT

Julia 使用找到的第一个此类路径尝试从找到的仓库中的文件 packages/Priv/HDKrT/src/Priv.jl 加载公共 Priv 包。

以下是根据上面给出的依赖项图中的清单,在搜索本地文件系统后,我们的示例 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 包位于 App 存储库内部的“供应商”中。
  2. 公共 PrivZebra 包位于系统仓库中,系统管理员安装和管理的包位于其中。系统上的所有用户都可以使用这些包。
  3. Pub 包位于用户仓库中,用户安装的包位于其中。只有安装这些包的用户才能使用它们。

包目录

包目录提供了一种更简单的环境类型,而没有处理名称冲突的能力。在包目录中,顶级包的集合是“看起来像”包的子目录的集合。如果目录包含以下“入口点”文件之一,则包 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 条目,则uuid是通过对X/Project.toml的规范(真实)路径进行哈希生成的虚拟 UUID。
  3. 否则(如果Project.toml不存在),则uuid为全零空 UUID

依赖关系图由每个包的子目录中项目文件的存在及其内容决定。规则如下

  • 如果包子目录没有项目文件,则它将从图中省略,并且其代码中的导入语句将被视为顶级,与主项目和 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 严格是内部的。

在我们的示例中观察这些规则的一些特定实例

  • Aardvark可以导入BobcatCobraDingo中的任何一个;它确实导入了BobcatCobra
  • Bobcat可以并且确实导入了CobraDingo,它们都具有带有 UUID 的项目文件,并在Bobcat[deps]部分中声明为依赖项。
  • Bobcat不能依赖于Aardvark,因为Aardvark没有项目文件。
  • Cobra可以并且确实导入了Dingo,它具有项目文件和 UUID,并在Cobra[deps]部分中声明为依赖项。
  • Cobra不能依赖于AardvarkBobcat,因为它们都没有真实的 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映射条目始终具有此形式。

环境栈

第三种也是最后一种环境类型是通过叠加多个环境来组合其他环境,使每个环境中的包在一个组合环境中可用。这些组合环境称为环境栈。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优先考虑最后一个参数而不是第一个。此设计的几个值得注意的功能

  1. 主环境——即栈中的第一个环境——忠实地嵌入到栈式环境中。栈中第一个环境的完整依赖关系图保证包含在栈式环境中,包括所有依赖项的相同版本。
  2. 非主环境中的包最终可能会使用不兼容版本的依赖项,即使它们自己的环境完全兼容。当它们的依赖项之一被栈中较早环境中的版本覆盖时(通过图或路径,或两者兼而有之),就会发生这种情况。

由于主环境通常是您正在处理的项目的环境,而栈中后面的环境包含其他工具,因此这是正确的权衡:最好破坏您的开发工具,但保持项目正常运行。当发生此类不兼容性时,您通常需要将您的开发工具升级到与主要项目兼容的版本。

包扩展

包“扩展”是一个模块,当在当前 Julia 会话中加载一组指定的其他包(其“扩展依赖项”)时,它会自动加载。扩展在项目文件的[extensions]部分下定义。扩展的扩展依赖项是项目文件的[weakdeps]部分下列出的那些包的子集。这些包可以像其他包一样具有兼容性条目。

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.jlext/FooExt/FooExt.jl中,用于扩展FooExt。扩展的内容通常结构如下

module FooExt

# Load main package and extension dependencies
using MyPackage, ExtDep

# Extend functionality in main package with types from the extension dependencies
MyPackage.func(x::ExtDep.SomeStruct) = ...

end

当将具有扩展的包添加到环境时,weakdepsextensions部分将存储在该包的部分中的清单文件中。包的依赖项查找规则与其“父”包相同,只是列出的扩展依赖项也被视为依赖项。

包/环境首选项

首选项是影响环境中包行为的元数据字典。首选项系统支持在编译时读取首选项,这意味着在代码加载时,我们必须确保 Julia 选择的预编译文件是在与当前环境相同的首选项下构建的,然后才能加载它们。修改首选项的公共 API 包含在Preferences.jl包中。首选项作为 TOML 字典存储在当前活动项目旁边的(Julia)LocalPreferences.toml文件中。如果首选项是“导出的”,则它将存储在(Julia)Project.toml中。目的是允许共享项目包含共享首选项,同时允许用户自己使用LocalPreferences.toml文件中的自己的设置覆盖这些首选项,该文件应该像名称暗示的那样被.gitignore。

在编译期间访问的首选项会自动标记为编译时首选项,并且对这些首选项记录的任何更改都会导致 Julia 编译器重新编译该模块的任何缓存的预编译文件(.ji和相应的.so.dll.dylib文件)。这是通过在编译期间序列化所有编译时首选项的哈希值,然后在搜索要加载的正确文件时将该哈希值与当前环境进行比较来完成的。

首选项可以使用仓库范围的默认值设置;如果包 Foo 安装在您的全局环境中并且它设置了首选项,则只要您的全局环境是您LOAD_PATH的一部分,这些首选项就会应用。环境栈中较高位置的环境中的首选项会被加载路径中更接近的条目覆盖,最终覆盖当前活动项目。这允许存在仓库范围的首选项默认值,并且活动项目能够合并甚至完全覆盖这些继承的首选项。有关如何设置首选项以允许或不允许合并的完整详细信息,请参阅Preferences.set_preferences!()的文档字符串。

结论

联合包管理和精确的软件可重复性是包系统中困难但值得的目标。结合起来,这些目标导致比大多数动态语言更复杂的包加载机制,但它也产生了更常与静态语言相关的可扩展性和可重复性。通常,Julia 用户应该能够使用内置的包管理器来管理他们的项目,而无需精确理解这些交互。对Pkg.add("X")的调用将添加到通过Pkg.activate("Y")选择的适当的项目和清单文件中,以便将来对import X的调用将加载X,无需进一步考虑。