依赖

依赖

2019年10月26日

在过去的20年中,人们对编程方法进行了重大转变。过去,人们更注重在每个项目中编写可重用和可扩展的组件,而目前的趋势是创建小型、独立的项目,每个项目都易于丢弃和替换。当前的微服务实现和 JavaScript 依赖树可能会让我们认为,没有哪个项目小到不能被丢弃。

具有讽刺意味的是,向强隔离和明确定义的边界(无论是接口、网络 API 还是协议)的转变,这对小型单一用途组件来说是必要的,催生了我们可能见过的最大程度的代码重用。重用来自不同人员和项目之间共享的需求,而不是来自提供完美可扩展类层次结构的神话般的能力。如今的重用如此之多,以至于人们开始质疑,也许,仅仅也许,我们重用的代码是否超过了我们应该重用的程度:每个库都伴随着风险和责任,而为了快速发展,我们正在暴露自己面临很多风险。

Erlang 本身一直都在遵循强隔离和明确定义的消息传递协议(封装在函数式接口中)。其规模较小且通常由经验丰富的开发人员组成的社区,在库和打包方面可能落后于其他社区。尽管 ProcessOne 和 Faxien 之前曾努力开发像 CEAN 这样的包管理器(两者都全局安装 OTP 应用),但直到 2009 年,安装其他人的库才真正变得容易。这得益于 rebar 的第一个版本,它偏好每个项目的依赖项安装,并且是第一个以 escript 形式出现的,这是一个可移植的脚本,开发人员无需安装。

Erlang 库在 GitHub 和类似的托管版本控制服务上杂乱无章地大量涌现,在某个时刻,人们可以找到十多个不同版本的相同 PostgreSQL 驱动程序,它们都具有相同名称和类似版本,但都做了一些不同的事情。

Elixir 的大力推动,它带来了更新的视角和工具(如 mix 和 hex.pm),促使 Erlang 社区重新团结起来,构建了一个更易于理解的生态系统。在现代,Erlang 的社区仍然很小,但它已经采用了更好的实践,现在与 Elixir 和同一虚拟机上的其他六种语言共享其包和库基础设施。

在本章中,我们将了解 Erlang 世界中如何实现现代库的使用和社区集成,方法是回答以下问题:

  • 什么是库?
  • 如何将库用作依赖项?
  • 依赖项的生命周期是什么?
  • 如何使用 Elixir 依赖项?
  • 如果我的工作使用单体仓库,我该怎么办?

使用开源库

Erlang 的开源依赖项只是 OTP 应用,就像发布版本中的任何其他库一样。因此,使用开源库所需做的全部工作就是让 Erlang 工具链能够访问该 OTP 应用。从概念上讲,这很简单,但每种语言和社区在这里都采用了略微不同的方法。我们需要在计算机上全局安装库、在共享环境中安装库或在每个项目中本地安装库之间做出选择。然后,还有一些关于版本控制和发布的规范,也必须遵守。本节将展示如何完成所有这些操作,但首先我们将绕道了解 Rebar3 对项目生命周期的期望。

Rebar3 期望

围绕开源工作的一些棘手决策已在 Rebar3 中得到体现,并且通常顺其自然比对抗它更容易,尤其是在你刚开始使用时。Rebar3 最初是在一个中等规模的公司中构建的,该公司的服务以半私有的方式编写:使用和发布开源依赖项,但某些代码将永远保持私有。同时开发多个不相关的服务,并非所有服务都一定使用相同版本的 Erlang 和相同的库。一些程序将仅通过滚动重启进行部署(这在云中很常见),但某些系统绝对需要热代码加载。Rebar3 也是在一个社区版本控制实践非常混乱的时期开发的:许多库在其 .app 文件中的版本与 GitHub 上的 git 标签不同,而文档又提到了另一个版本。当时,hex.pm 上大约五分之四的库甚至还没有达到 1.0.0 版本。

因此,Rebar3 具有以下特性:

  • Rebar3 是一个声明式构建工具。你提供配置,它执行代码来满足该配置。如果你想运行自定义脚本并扩展构建,它专门提供了 钩子插件 接口来做到这一点。还有一些方法可以获得 动态配置 以更灵活地填充配置文件。
  • 作为该声明式方法的一部分,Rebar3 中的命令使用依赖项序列定义。例如,rebar3 compile 任务依赖于 rebar3 get-deps(或者更确切地说,是其锁定依赖项的私有形式),并且会为你运行它。命令 rebar3 tar 会隐式调用一个包含 get-deps -> compile -> release -> tar 的序列。因此,Rebar3 知道要编译你的项目,需要它的依赖项。
  • Rebar3 在 _build 目录中定义了自己的工作区;它希望控制其中包含的内容,并且你不应该在源代码控制中跟踪该目录,也不依赖于其内部结构。当它创建用户想要直接使用的工件时,它会在终端中输出该工件的路径。例如,rebar3 escriptize 会在每次运行后打印生成的 escript 在 _build/default/bin/ 中的路径,rebar3 ct 会打印 Common Test HTML 输出的路径(如果测试失败),而 rebar3 tar 会打印发布包的路径。
  • 所有项目都基于当前加载到你的环境中的 Erlang 运行时(即 $PATH 中找到的 erl)在各自的目录中本地构建。
  • 所有依赖项都获取到项目的 _build 目录中。
  • 所有依赖项都可以定义自己的依赖项,Rebar3 会识别这一点并将它们也获取到根项目的 _build 目录中。
  • 由于版本号不可靠(即使使用语义版本控制),我们将版本视为供人类使用,而不是构建工具。如果库最终被多次声明,则选择最靠近项目根目录的库,并在第一次构建时发出 警告(它知道这是“第一次”,因为没有依赖项的锁定),让用户知道哪些版本未使用,并假设声明得更靠近根项目的库使用得更彻底。
  • 禁止循环依赖。
  • Rebar3 支持可组合的每个项目的 配置文件,允许你细分或组合配置设置,例如仅用于测试或仅在特定环境(例如特定目标操作系统)中使用的依赖项。配置文件的数量和名称没有限制,但有四个配置文件,defaulttestdocsprod,Rebar3 会自动将它们用于特定任务。
  • 构建旨在可重复。依赖项被锁定,并且仅在明确要求升级时才升级。rebar.lock 中的版本优先于 rebar.config 中声明的任何版本,直到调用 rebar3 upgrade 更新锁定文件。
  • 依赖项始终使用应用的 prod 配置文件进行构建。Rebar3 将始终检查它们是否与锁定文件匹配以及构建工件是否存在,但不会检测手动在依赖项中完成的代码更改。
  • Rebar3 假设你有时需要调整你无法控制的库的配置,并为此目的支持 覆盖
  • 该工具假设你使用源代码控制机制(如 githg(Mercurial))进行开发,这意味着切换分支可能会切换锁定文件中的依赖项版本。由于 Rebar3 在每次构建之前都会验证依赖项,因此如果切换分支时发生更改,它会自动重新获取库以获取当前分支的锁定版本。
  • 为了方便贡献和发布,Rebar3 本身不支持使用相对路径来声明库,因为这可能会使构建变得脆弱、不可重复且在发布代码时不可移植。
  • 知道相对路径在项目依赖项中进行更改时非常常见,_checkouts 允许以自动方式使用本地副本临时覆盖依赖项。对于其他用例,插件 允许创建 自定义资源类型
  • Rebar3 不是最终用户应用的安装程序或运行程序,也不支持与此相关的任何内容;它的目标是生成构建工件,然后你可以通过正确的专用渠道安装这些工件。Rebar3 不期望也不需要在生产设备或服务器上运行。
  • Rebar3 不是沙箱。虽然它会确保你下载的所有依赖项都与正确的签名匹配并提供可重复的构建,但它不能保证构建期间调用的脚本文件或编译期间运行的 解析转换 始终安全,并且不打算承担此责任。插件也不会自动锁定,库作者有责任在插件影响编译的情况下固定版本。

这些信息很多,但我们发现,在深入了解依赖项之前了解这些信息很有用。如果你在 Rebar3 的工作原理方面存在与 Javascript 的 npm、Elixir 的 mix、Go 的工具链甚至原始 rebar 相似的假设,你可能会发现某些行为令人困惑。话虽如此,让我们开始使用依赖项吧。

声明依赖项

由于依赖项都是项目本地化的,因此必须在项目的 rebar.config 文件中声明它们。这将使 Rebar3 知道需要获取它们、构建它们并使它们可用于你的项目。所有依赖项都必须是独立的 OTP 应用,以便可以独立于彼此进行版本控制和处理。

以下格式有效:

{deps, [
    %% git dependencies
    {AppName, {git, "https://host.tld/path/to/app", {tag, "1.2.0"}}},
    {AppName, {git, "https://host.tld/path/to/app", {branch, "master"}}},
    {AppName, {git, "https://host.tld/path/to/app", {ref, "aed12f..."}}},
    %% similar format for mercurial deps
    {AppName, {hg, "https://host.tld/path/to/app", {RefType, Ref}}}
    %% hex packages
    AppName, % latest known version (as per `rebar3 update`)
    {AppName, "1.2.0"},
    {AppName, "~> 1.2.0"}, % latest version at 1.2.0 or above, and below 1.3.0
    {AppName, "1.2.0", {pkg, PkgName}}, % when application AppName is published with package name PkgName
]}.

此外,插件允许定义 自定义资源定义,让你可以向项目中添加新的依赖项类型。

让我们看看这在专门为本书创建的项目 service_discovery 中是如何工作的。打开 rebar.config 文件,你会看到:

...

{deps, [
    {erldns,
     {git, "https://github.com/tsloughter/erldns.git",
     {branch, "revamp"}}},
    {dns,
     {git, "https://github.com/tsloughter/dns_erlang.git",
     {branch, "hex-deps"}}},

    recon,
    eql,
    jsx,
    {uuid, "1.7.5", {pkg, uuid_erl}},
    {elli, "~> 3.2.0"},
    {grpcbox, "~> 0.11.0"},
    {pgo,
     {git, "https://github.com/tsloughter/pgo.git",
     {branch, "master"}}}
]}.

...

对于大多数项目,git 和 hex 依赖项可以一起工作。唯一的例外是 hex 包,它们只能依赖于其他 hex 包。让我们编译整个项目并逐步了解正在发生的事情。

$ rebar3 compile
===> Fetching covertool v2.0.1
===> Downloaded package, caching at /Users/ferd/.cache/rebar3/hex/hexpm/packages/covertool-2.0.1.tar
===> Compiling covertool
...
===> Verifying dependencies...
===> Fetching dns (from {git,"https://github.com/tsloughter/dns_erlang.git",
               {ref,"abc562548e8a232289eec06cf96ce7066261cc9d"}})
===> Fetching provider_asn1 v0.2.3
===> Downloaded package, caching at /Users/ferd/.cache/rebar3/hex/hexpm/packages/provider_asn1-0.2.3.tar
===> Compiling provider_asn1
===> Fetching elli v3.2.0
...
===> Fetching rfc3339 v0.9.0
===> Version cached at /Users/ferd/.cache/rebar3/hex/hexpm/packages/rfc3339-0.9.0.tar is up to date, reusing it
===> Compiling quickrand
===> Compiling uuid
===> Compiling recon
...
===> Compiling service_discovery_storage
===> Compiling service_discovery
===> Compiling service_discovery_http
===> Compiling service_discovery_grpc
===> Compiling service_discovery_postgres

运行此构建,你可以看到正在发生多件事:

  1. 插件(如 covertool)在任何其他操作开始之前都会被获取和编译。

  2. 项目实际依赖项(例如dnselli)被获取。
  3. 依赖项被编译(quickrand等)。
  4. 主应用程序被编译。

如果你尝试重新开始并从头运行,同时删除rebar.lock文件,情况会略有不同。你可能会在输出中看到类似这样的内容。

...
===> Fetching dns (from {git,"https://github.com/tsloughter/dns_erlang.git",
               {branch,"hex-deps"}})
...
===> Skipping dns (from {git,"git://github.com/dnsimple/dns_erlang.git",
               {ref,"b9ee5b306acca34b3d866d183c475d5f12b313a5"}}) as an app of the same name has already been fetched
...
===> Skipping jsx v2.9.0 as an app of the same name has already been fetched
===> Skipping recon v2.4.0 as an app of the same name has already been fetched
...

这些是在依赖项解析期间出现的少量通知和警告,当rebar.lock文件可用时则不需要。它们通知库维护者检测到冲突,并且跳过了某个版本的库。要完成构建的审核,你可以通过调用rebar3 tree来检查最终的依赖项解析。

$ rebar3 tree
===> Verifying dependencies...
├─ service_discovery─e4b7061 (project app)
├─ service_discovery_grpc─e4b7061 (project app)
├─ service_discovery_http─e4b7061 (project app)
├─ service_discovery_postgres─e4b7061 (project app)
└─ service_discovery_storage─e4b7061 (project app)
   ├─ dns─0.1.0 (git repo)
   │  └─ base32─0.1.0 (hex package)
   ├─ elli─3.2.0 (hex package)
   ├─ eql─0.2.0 (hex package)
   ├─ erldns─1.0.0 (git repo)
   │  ├─ iso8601─1.3.1 (hex package)
   │  ├─ opencensus─0.9.2 (hex package)
   │  │  ├─ counters─0.2.1 (hex package)
   │  │  └─ wts─0.3.0 (hex package)
   │  │     └─ rfc3339─0.9.0 (hex package)
   │  └─ telemetry─0.4.0 (hex package)
   ├─ grpcbox─0.11.0 (hex package)
   │  ├─ acceptor_pool─1.0.0 (hex package)
   │  ├─ chatterbox─0.9.1 (hex package)
   │  │  └─ hpack─0.2.3 (hex package)
   │  ├─ ctx─0.5.0 (hex package)
   │  └─ gproc─0.8.0 (hex package)
   ├─ jsx─2.10.0 (hex package)
   ├─ pgo─0.8.0+build.91.refaf02392 (git repo)
   │  ├─ backoff─1.1.6 (hex package)
   │  └─ pg_types─0.0.0+build.24.ref32ed140 (git repo)
   ├─ recon─2.4.0 (hex package)
   └─ uuid─1.7.5 (hex package)
      └─ quickrand─1.7.5 (hex package)

此列表以Verifying dependencies ...行开头,Rebar3 在打印树之前验证所有依赖项是否已解析。接下来是所有顶级应用程序(我们在存储库中编写的那些),我们刚刚获取的依赖项都在它们下面。你可以看到整个解析树,找到哪些版本已被获取以及哪个应用程序引入了它们。这可以帮助理解当两个或多个应用程序包含传递依赖项时,为什么选择了某个特定版本。

如果你查看_build/default/lib,你会看到所有这些应用程序都在各自的目录中。

$ ls _build/default/lib
acceptor_pool  gproc       rfc3339
backoff        grpcbox     service_discovery
base32         hpack       service_discovery_grpc
chatterbox     iso8601     service_discovery_http
counters       jsx         service_discovery_postgres
ctx            opencensus  service_discovery_storage
dns            pgo         telemetry
elli           pg_types    uuid
eql            quickrand   wts
erldns         recon

这些都是具有类似目录结构的OTP应用程序。这种布局与OTP高级概述中描述的发布项目结构非常相似,但这仍然只是一个暂存区域。

构建具有依赖项的项目

构建项目中的OTP应用程序不仅需要获取其依赖项并进行编译。如什么是库和应用程序中所述,Erlang运行时系统期望在.app文件中找到依赖项的运行时定义。如果没有将其放在那里,则告诉Rebar3它们是构建时依赖项,而不是运行时依赖项。这意味着它们不会包含在某些任务和环境中:例如,发布将忽略它们,并且rebar3 dialyzer将避免将其包含在其分析中。

打开apps/service_discovery/src/service_discovery.app.src并查看applications元组中的值。

{application, service_discovery,
 [{description, "Core functionality for service discovery service"},
  {vsn, {git, short}},
  {registered, []},
  {mod, {service_discovery_app, []}},
  {applications,
   [kernel,
    stdlib,
    erldns,
    service_discovery_storage
   ]},
  {env,[]},
  {modules, []},

  {licenses, ["Apache 2.0"]},
  {links, []}
 ]}.

你可以看到已添加了erldnsservice_discovery_storage。指定这些依赖项可确保它们在运行时和发布中可用。不将其放在那里可能会导致构建失败。

如果你曾经使用过Erlang生态系统中的其他构建工具,你可能从未需要这样做。这些工具(erlang.mk或elixir中的Mix)最终会将项目配置中的依赖项复制到applications元组中。手动执行此操作听起来像是很大的障碍,但它最终遵循了OTP标准以支持构建时依赖项。其他工具通过配置文件中的其他选项达到类似的结果。让我们看看Rebar3的方法在哪些情况下可以提供关键的控制。

第一个重要的情况是下载你希望包含在发布版中以帮助你调试的依赖项,但你的任何OTP应用程序都不依赖于它。例如reconredbug或自定义的logger处理程序。你希望这些应用程序在发布版中可用,但由于applications元组让发布版知道必须按什么顺序引导或启动应用程序,因此你并不一定希望这些应用程序成为你的依赖链的一部分。为什么一个只是以防万一安装的调试工具需要为了网站的正常工作而处于运行状态?这完全没有必要。你不会希望一个故障的调试工具阻止你的实际应用程序启动。

在这种情况下,你希望项目配置看起来像service_discovery的这一部分。

{relx, [
    {release, {service_discovery, {git, long}},
     [service_discovery_postgres,
      service_discovery,
      service_discovery_http,
      service_discovery_grpc,
      recon]},
    ...
]}.

你可以看到,除了我们的应用程序外,recon调试工具还明确包含在发布版的应用程序列表中。所有它们的传递依赖项都将被包含(根据.app文件中的applications元组),但各种OTP应用程序以分离的方式处理。

让我们稍微关注一下这4个service_discovery应用程序。它们代表了第二种情况,在这种情况下,我们希望拆分rebar.config中构建的依赖项声明和.app.src文件中运行时依赖项的声明。

你可以看到,在顶级,所有库的所有依赖项都在单个rebar.config文件中声明。这使得开发人员可以轻松地处理和更新所有所需的版本。但是,如果你查看service_discoveryservice_discovery_http.app.src文件,你会发现以下内容。

%% service_discovery.app.src
...
  {applications,
   [kernel,
    stdlib,
    erldns,
    service_discovery_storage
   ]},
...
%% service_discovery_http.app.src
...
  {applications,
   [kernel,
    stdlib,
    service_discovery,
    jsx,
    elli
   ]},
...

在这里,service_discovery_http依赖于一个Web服务器(elli),但service_discovery不依赖。这允许更清晰的启动和关闭场景,在这些场景中,你实际上不需要Web服务器启动和运行即可启动系统的后端。

对于第三种情况,你还可以想象一个小型应用程序service_discovery_mgmt,它仅用于生成一个escript,让你可以执行系统管理任务以与系统交互并发送命令。

如果运行时依赖项在所有依赖于相同rebar.config文件的应用程序之间共享,那么即使service_discovery_mgmt包含在发布版中(它只是一个附带的脚本),它的依赖项仍然可能通过其他应用程序自动插入到其中而被推送到生产环境。更糟糕的是,service_discovery发布版的所有依赖项也可能与脚本捆绑在一起!最终,我们可能会得到一个包含Web服务器和数据库驱动程序的小型管理工具,因为构建工具试图变得友好。

因此,Rebar3维护者只是决定在项目构建或运行所需获取的应用程序(在rebar.config中)和每个OTP应用程序的运行时依赖项(在.app文件中)之间保持清晰的区分,这些依赖项可能是默认OTP安装的一部分,因此不会包含在rebar.config中。生态系统中的其他构建工具允许你获得类似的结果,但它们默认情况下会在运行时包含所有内容,而Rebar3要求开发人员始终明确他们的意图。

有了所有这些,我们只需要整理和清理我们的依赖项集。

依赖项生命周期

在初始化项目时,依赖项解析和获取是最繁重的工作。结果存储在锁定文件中,之后的所有结果都通过Rebar3完成的较短的部分更改来处理。即使你很少需要执行此操作,了解这个初始阶段也很重要。

Rebar3锁定文件创建在项目的根目录,即你调用Rebar3的目录。它保存为rebar.lock,你应该在你选择的源代码控制中跟踪它。你可以打开该文件并查看其内容,但通常不需要手动编辑它。你会发现它主要包含版本号、应用程序名称和各种哈希值。偶尔审核一下它可能很有趣,但你最终会间接地通过维护依赖项树来完成此操作。

锁定文件表示构建时所有依赖项的扁平化树。除非你要求更改它,或删除它强制从头开始新的解析,否则它不会被修改。这种严格性是有目的的,并且是Rebar3如何在任何情况下都能保证可重复构建的一部分。

文件中的哈希值意味着,即使依赖项由多层镜像获取,并且某些恶意人员更改了你在各种hex索引或git源中使用的其中一个或多个包,Rebar3也将能够发现信息与预期不符,并因此出错。

因此,你应该仅在需要时更新锁定文件。你可以使用以下操作来做到这一点。

  • rebar3 unlock <appname>从锁定文件中删除未使用的依赖项。你通常希望在已从rebar.config文件中删除它之后调用此命令,以告诉Rebar3它确实已消失或已降级为传递依赖项。
  • rebar3 upgrade <appname>告诉Rebar3忽略该应用程序的锁定版本,并从当前在rebar.config中指定的版本(如果有)重新构建依赖项树。这将生成一个新的锁定文件并重新解析可能已更改的所有传递依赖项。
  • rebar3 update虽然严格来说与锁定文件无关,但它更新了远程hex包的本地快照,这是一种缓存,可以防止每次构建都ping包服务器。如果你发现自己在某个应用程序上调用rebar3 upgrade,但它没有升级到你已知在Hex中可用的最新版本,则需要先update。这是因为Rebar3通过尝试使用本地索引缓存来解析依赖项,从而限制了网络的使用。rebar3 update将获取索引中每个包的最新索引条目,然后另一次运行upgrade将看到最新版本。请注意,如果你指定要升级到的确切版本,Rebar3将自动获取更新的索引,因为它无法在本地满足依赖项。
  • rebar3 tree打印出已构建并由锁定文件表示的依赖项树。
  • rebar3 deps列出依赖项并注释可以更新的依赖项。请注意,它在认为值得更新的内容方面存在很大的限制:git中的分支、参考已更改的标签或未指定版本的hex版本。但是,如果你指定某个包的版本为"1.2.3"并且1.2.4可用,它不会告诉你任何信息。

还有其他一些更激进的命令,即rebar3 unlockrebar3 upgrade(不带任何参数)。这些命令只会删除项目下次构建的锁定文件。但总的来说,所有这些命令都能满足你管理大多数依赖项的需求。

通常,你的工作流程可能如下所示。

  1. 设置初始项目,编译一次,并在源代码控制中跟踪锁定文件。
  2. 发现你想要更改一个顶级依赖项。
  3. 更改rebar.config文件中的依赖项定义(或者如果使用非版本化的hex包,可以选择调用rebar3 update)。
  4. 调用rebar3 upgrade <app>以更新应用程序及其传递依赖项。

就是这样,你完成了。

检出依赖项

Rebar3希望使开发人员的生活更轻松,同时保持安全和可重复性。_checkouts是一个与可重复性和锁定文件等概念相反的功能,但它提供了快速反馈和围绕本地依赖项更改的更好体验。

如果你刚开始使用 Erlang,无论是出于兴趣还是工作需要,你可能会有几个依赖项位于单独存储库中的项目。处理它们不会太难。但迟早,如果你需要开始修补依赖项,或者你正在一个拥有数十个存储库的企业环境中工作,使用 Rebar3 可能会变得令人沮丧。

问题在于,每次你想尝试修改依赖项时,都必须将更改提交并发布到 Rebar3 可以获取的地方,因为它不会构建对 _build 下源文件所做的更改,如果该模块已经存在相应的 .beam 文件。当您需要跨存储库边界工作并且只想测试更改时,这很快就会变得令人恼火。

因此,有一个名为 检出依赖项 的小技巧功能。检出依赖项的工作原理如下:

  • 你的主项目位于文件系统中的某个位置
  • 依赖项在主项目的 rebar.config 文件中声明
  • 你还在文件系统的某个位置拥有该依赖项,作为一个独立的项目
  • 你在主项目中添加一个 _checkouts/ 目录
  • 你将依赖项的目录复制或符号链接到 _checkouts/ 目录中

从那时起,每次构建主应用程序时,它都会在 checkouts 中依赖项的目录中添加一个 ebin/ 目录,并将其重新编译,就像它是主项目中的顶级应用程序一样。

然后,你可以在主项目中测试对依赖项的更改,直到它们准备好。完成后,从依赖项中删除 ebin/ 目录,将代码提交并发布到依赖项中,将其从 _checkouts 目录中删除,并使用 rebar3 upgrade 进行升级。

这使你能够在本地对依赖项进行许多小的迭代更改,在主项目的上下文中,而无需推送依赖项的更改,也无需更改配置文件以将依赖项指向某些本地目录。作为整体开发策略,它极大地减少了每个应用程序存储库的开销。

使用 Elixir 依赖项

多年来,Elixir 和 Erlang 的道路是单行道。你可以在你的 Elixir 项目中包含 Erlang 依赖项,但反过来却不行。从那时起,感谢 Erlang 生态基金会的支持,Rebar3 已经进行了更改,使其拥有一个全新的编译器管理结构。这种结构使得编写编译器插件成为可能,使 Erlang 用户能够使用 Elixir 代码。

方法是首先安装 Elixir。为此,你可能需要按照 Elixir 官方网站上的步骤 进行操作。大多数 Elixir 开发人员使用 asdf 作为版本管理工具。如 设置 中所述,Erlang 的 kerl 选项可以与 asdf 的 Erlang 插件一起使用,因此可以为你提供完整的设置。

安装好 Elixir 后,将 rebar_mix 插件添加到你的库或项目中

{plugins, [rebar_mix]}.
{provider_hooks, [{post, [{compile, {mix, consolidate_protocols}}]}]}.

{relx, [
    ...
    {overlay, [
        {copy, "{{base_dir}}/consolidated", "releases/{{release_version}}/consolidated"}
    ]
}.

如果你的项目有 vm.args.src 文件,请在其中添加以下行

-pa releases/${REL_VSN}/consolidated

从那时起,你将能够毫无问题地安装任何包含 Elixir 代码的 hex 依赖项。目前,该插件仅支持也仅依赖于其他 hex 依赖项的 hex 依赖项;但是,使用 git 的传递依赖项的支持即将推出。

请注意,目前不支持在同一库中使用 Erlang 和 Elixir 的混合 Rebar3 项目,因为需要在 Rebar3 和 Elixir 方面做更多工作才能实现这一点。另一方面,如果你需要这种模式,Mix 支持它。

企业环境

企业环境往往对可以或不可以做什么有各种奇怪的限制,并且开发工具可能非常特殊。Rebar3 主要是在适应开源世界的情况下开发的,主要侧重于强制执行项目结构、获取依赖项以及将两者结合使用作为包装一堆标准工具的一个很好的借口。

因此,用该工具适应企业环境可能会有点挑战也就不足为奇了。在本节中,我们将介绍一些企业环境中常见的标准工具,这些工具在采用 Erlang 时可能会让你的生活更轻松。

代理支持

许多工作场所实施非常严格的防火墙规则,以至于所有传入和传出的数据都必须被拦截和监控。通常,这些地方不会完全与公共互联网隔绝,但需要使用代理服务器来进行传出连接。

程序尊重 HTTP_PROXYHTTPS_PROXY 环境变量是相当标准的做法。当这些变量在你的开发环境中设置时,Rebar3 将确保它与外部世界通信时使用的所有通信都使用这些代理。

这应该让你能够根据 IT 部门的政策正常工作。在某些情况下,这甚至还不够。

私有 Hex 镜像

一些公司更进一步,将他们的内部网络与公共互联网隔离开来。所有进入现场的数据都必须经过检查并独立托管,而没有机会与像 github、gitlab 或 hex 这样的公共代码存储库进行通信。另一个有趣的案例是构建服务器,你可能希望出于安全和可重复性原因阻止所有与外部世界的连接。

对于此类设置,通常使用两种方法:在 monorepo 中进行供应商管理(将在下一节中介绍)以及通过私有托管的包索引(我们将在本文中介绍)。

私有托管索引背后的理念是,项目中要使用的所有包和依赖项都需要经过全面审查。你可能希望对其进行代码质量的技术审查、安全评估,或让公司律师查看代码的许可或专利问题。然后,只能使用正确版本的包。这种索引通常被接受用于确定性构建,假设它要么在每个构建服务器上本地运行,要么在与构建服务器一样严格监控的私有网络中运行。

Rebar3 支持此用例。如果要启用它,则需要首先设置一个私有 hex 实例。这可以通过 minirepo 项目来完成。按照 项目页面上的说明,你最终将运行你自己的私有 hex 服务器,使用本地文件系统或基于 S3 的存储,以及镜像其他索引和私下发布你自己的包的功能。

你需要 调整你的全局 Rebar3 配置 以使用它,但一旦完成,你就可以开始了。

关于 Monorepos

如果你在一个企业环境中使用 monorepo,其中所有私有库、OTP 应用程序和依赖项都被平等对待(它不仅仅是一个伞形发布),那么 Rebar3 不是这项工作的最佳工具。这主要归结于这样一个事实,即大多数使用 monorepo 的公司都拥有许多具有非常自定义工作流程的自定义工具、大型代码库以及极强的倾向,即永远不会与 Rebar3 的维护人员共享访问权限。在维护人员能够获得访问权限之前,在这方面几乎不可能做任何事情。

尽管使用 monorepo,但各种商业用户报告说,通过结合使用 _checkouts 依赖项以及重新配置 _build 目录,可以获得成功的构建。但是,我们目前不建议使用这种方法,并且不提供对 monorepo 的官方支持。

另一种选择是供应商管理依赖项,这可以通过诸如 rebar3_path_deps 之类的插件来完成。

有了所有这些,你应该可以设置管理项目依赖项的生命周期了。