【Blog】Cabal 简易指南 - 2023

"这是我写于 2023 年的关于 Cabal 的简易使用指南,包括关于 cabal install 和 npm install 不同的地方的解惑。"


本文使用的 Cabal 版本为 3.10.1.0。本文只包含它的简易使用指南、简易机制解惑和一些作者自己使用时遇到过的疑惑的简单解答,不包括安装教程。

作者推荐使用 GHCUP 进行安装与后续的版本管理。

Cabal 是一个应用于 Haskell 开发的包管理器。它所安装的包的默认来源为 Haskell 社区中的包仓库 Hackage (hackage.haskell.org)。因此,所有在Hackage上能够被搜索到的包都可以使用 Cabal 进行安装。

1.0 简易起步

要使用 Cabal 作为包管理器进行 Haskell 程序开发,首先你要使用 Cabal 创建对应的工程目录:

cabal init foldername

这条命令将会在指定目录初始化相应的工程项目,也就是一个 Cabal Package,一个包。 当然,你也可以直接在当前目录下新建项目:

cd test
cabal init

默认情况下,cabal init 会默认使用 --interactive 参数在交互模式下启动。这一模式下,cabal会和 nodejs 社区的包管理器 npm 一样,详细询问你相应的配置项:

-- 首先会询问你想构建的项目的类型
What does the package build:
   1) Library -- 如果你是想创建其他 Haskell 项目可以复用的库,选取此项
 * 2) Executable -- 如果你只是想创建一个 Haskell 程序,选取此项
   3) Library and Executable -- 如果你构建的项目同时包括程序与库,选取此项
   4) Test suite -- 测试单元
Your choice? [default: Executable] -- 默认情况会是 Excutable,构筑 Haskell 程序
 
-- 是否覆写指定目录现有的文件
Do you wish to overwrite existing files (backups will be created) (y/n)? [default: n]
 
-- 选择projectname.cabal文件的诠释版本
Please choose version of the Cabal specification to use:
   1) 1.24  (legacy)
   2) 2.0   (+ support for Backpack, internal sub-libs, '^>=' operator)
   3) 2.2   (+ support for 'common', 'elif', redundant commas, SPDX)
   4) 2.4   (+ support for '**' globbing)
 * 5) 3.0   (+ set notation for ==, common stanzas in ifs, more redundant commas, better pkgconfig-depends)
   6) 3.4   (+ sublibraries in 'mixins', optional 'default-language')
Your choice? [default: 3.0]
 
-- 包的名称,或者说项目的名称
Package name? [default: test]
 
-- 包的版本
Package version? [default: 0.1.0.0]
 
-- 选择对应的开源许可证
Please choose a license:
   1) BSD-2-Clause
   2) BSD-3-Clause
   3) Apache-2.0
   4) MIT
   5) MPL-2.0
   6) ISC
   7) GPL-2.0-only
   8) GPL-3.0-only
   9) LGPL-2.1-only
  10) LGPL-3.0-only
  11) AGPL-3.0-only
  12) GPL-2.0-or-later
  13) GPL-3.0-or-later
  14) LGPL-2.1-or-later
  15) LGPL-3.0-or-later
  16) AGPL-3.0-or-later
  17) Other (specify)
Your choice?
 
-- 包的作者名
Author name? [default: Wendaolee]
 
-- 维护者邮箱
Maintainer email? [default: leewendao@outlook.com]
 
-- 项目主页地址
Project homepage URL? [optional]
 
-- 项目的简介
Project synopsis? [optional]
 
-- 项目的类别
Project category:
   1) Codec
   2) Concurrency
   3) Control
   4) Data
   5) Database
   6) Development
   7) Distribution
   8) Game
   9) Graphics
  10) Language
  11) Math
  12) Network
  13) Sound
  14) System
  15) Testing
  16) Text
  17) Web
  18) Other (specify)
Your choice? [default: (none)]
 
-- 入口文件
What is the main module of the executable:
 * 1) Main.hs
   2) Main.lhs
   3) Other (specify)
Your choice? [default: Main.hs]
 
-- 源代码目录
Application directory:
 * 1) app
   2) exe
   3) src-exe
   4) Other (specify)
Your choice? [default: app]
 
-- 在哪种 Haskell 标准上编写程序
Choose a language for your executable:
 * 1) Haskell2010
   2) Haskell98
   3) GHC2021 (requires at least GHC 9.2)
   4) Other (specify)
Your choice? [default: Haskell2010]
 
-- 是否在project.cabal文件中添加各个配置项的注释(个人建议为y)
Add informative comments to each field in the cabal file. (y/n)? [default: y]

如果你只是单纯想写一个 Haskell 应用,可以使用 cabal init --n (cabal init --non-interactive)。它会直接生成一个 Haskell 应用的包目录。

根据上面的步骤生成的目录结构如下:

│-  CHANGELOG.md --应用更改的更新日志模板
│- projectname.cabal -- cabal配置文件 

├─app  -- 源代码目录,根据你在init时的值的设置的不同,源代码目录名也会不同
      Main.hs

└─dist-newstyle -- 编译后出现的输出目录

如果您想编译您的项目,只需在当前目录下执行:

cabal build

它会编译您的项目,将可执行文件生成于同目录的 dist-newstyles/build/compile-info-paths... 之中。

如果您想运行您的项目,则是执行:

cabal run

它会编译并运行您的项目。

在通过cabal init生成的目录里,唯一需要你注意的是 projectname.cabal 文件。它是 Cabal 管理你的包的重要凭证,就像是 package.json 之于 npm ,同时你可以在其中修改你在交互模式中填入的一些信息。

里面的内容应该初中生就能读懂,在此不做详细翻译。唯一需要你注意的是这几项配置:other-modules ,build-depends :

  1. other-modules 用于指定在程序中使用的模块。也就是说,想要对代码进行模块化,那么在使用 Cabal 的工程项目中必需把每个模块在projectname.cabal文件中的other-modules项阐明。例如你要写一个DataLayer.Type模块,那么便需要在该项添加DataLayer.Type
  2. build-depends 用于指定程序需要使用的第三方库。cabalnpm是不一样的。如果你要使用第三方库,你需要手动在这一项中指定该库。

build-depends这一配置项与cabal install 是令人迷惑的。习惯于使用类似npm的包管理器可能会通过npm install package-name为当前项目安装依赖,然而在 cabal 中您只能通过更改 projectname.cabal 文件的 build-depends 项来在自己的项目中使用第三方库。

关于这一点在下面一节中会提供详细的解释。在这里,我只为您阐述清楚在自己的项目中如何引入第三方库:

在终端里,运行:

-- libname是第三方库的名称<-这一条是注释啊,不要会错意了
cabal install --lib libname 

而后在 projectname.cabal中添加该库:

-- Other library packages from which modules are imported.
-- 引入多个库时,使用英文逗号分割。您同样可以使用 = 、 ^>= 等符号指定库的版本
 
    build-depends:    base ^>=4.14.3.0,libname

之后运行 cabal buildcabal run 时,cabal会自动引入相关依赖库完成编译与程序的运行。

当然,您也可以直接在 projectname.cabal 文件的 build-depends 声明该库,cabal 会在执行 buildrun 时自动安装相关依赖库完成编译与程序的运行。但是我个人推荐预先通过cabal install --lib libname 安装库,因为这样你可以通过 LHS 在编写程序时获取相应的库的语法与类型支持。

2.0 简易 cabal install 解惑:cabal 的包管理机制

Haskell是一门编译型语言,这一特性间接使得 Cabal 不会和 npm 一样,存在着存放在工程目录的本地包文件夹 (如 npmnode_modules)。Cabal 的包永远是全局管理的。因此,在运行 cabal install package时,它实质上是在运行cabal install --global package (就像是 npm install -g package,请注意这里只是“释例”,cabal实际上并没有--global参数)。

一般来讲,当你键入 cabal install remote-package 时,会发生这些事:

  1. 从远程仓库中下载对应包的源码,将源码存放至 ~/cabal/packages目录中 。
  2. 而后将会编译这些源码为 Haskell 的 .hi 中间文件,存放到 ~/cabal/store 目录中。.hi 类似于 C 的 .o 文件,能够提高后续的编译速度。
  3. 如果你没有键入 cabal install --lib remote-package ,即没有在 cabal install 后补上 --lib 参数,cabal默认会将下载下来的源码包视为一个 Haskell 程序包,而不是一个第三方的Haskell库。因此它会尝试将该包编译为一个可执行程序,而后将输出的程序放到 cabal 的配置目录中(老版本的cabal会将文件放至~/cabal/bin,更为现代化的cabal会将文件放至~/.local/bin,根据当前cabal的配置而定)。

对于第三点,你可能会有些困惑。现在,让我们把目光回到开头。前边我们说过,如果使用交互模式运行cabal init,我们首先会得到初始化的包的类型:

What does the package build:
   1) Library -- 如果你是想创建其他 Haskell 项目可以复用的库,选取此项
 * 2) Executable -- 如果你只是想创建一个 Haskell 程序,选取此项
   3) Library and Executable -- 如果你构建的项目同时包括程序与库,选取此项
   4) Test suite -- 测试单元

也就是说,cabal的包主要是这三种类型:

  1. Haskell库 (library)
  2. Haskell程序 (executable)
  3. 测试单元

默认情况下,cabal install 实际上执行的是 cabal install --executable (这也只是一个“释例”!cabal install实际上并没有--executable参数)。它总是会尝试将目标的cabal包进行编译,而后尝试生成一个可执行程序。

这也是为什么当你尝试通过 cabal install lib 时,会出现这样的报错:

cabal install sqlite-simple
...
Error: cabal-3.10.1.0.exe: Cannot build the executables in the package
sqlite-simple because it does not contain any executables. Check the .cabal
file for the package and make sure that it properly declares the components
that you expect.

但实际上这并没有什么影响。事实上在出现这一报错时,相应的库已经安装到你的设备上了。

同理,当你在一个 cabal 工程的根目录下运行 cabal install 时,它实际上会尝试将该 cabal 工程生成为一个可执行程序安装到你的设备上。因此会发生这些事:

  1. 读取分析 projectname.cabal 文件,尝试引入 build-depends的库,而后进行编译。对于没有安装到本地的库,它会自动下载安装。(相当于 cabal build,这也是为什么 cabal install 也可以)
  2. 尝试将编译完的工程生成可执行程序,输出到相应的配置目录中。

这也同样是 cabal installnpm install 的不同之处: cabal install 是直接编译相应 cabal 包生成程序,这一过程中会自动下载安装相应的依赖库;而 npm install 仅仅只是下载相应的依赖库。

总结:

  1. 如果你想要安装第三方的 Haskell 库,可以使用 cabal install --lib libname
  2. 如果你要安装其他的,使用 cabal install xxxx 。它也可以完成安装库的任务,但它的语义并非是安装库。安装相应的依赖库只是它附带的。
  3. 如果你不确定你要安装的是什么,无脑 cabal install 即可。

事实上,cabal已经有计划将安装库与安装程序的命令进行区分,详情可见 Github Issue#6481。只是三年了依然没有推进......

3.0 参考 Reference

[1] What cabal install did when type this command.https://github.com/haskell/cabal/discussions/9210 [2]Cabal docs.https://cabal.readthedocs.io/en/latest/getting-started.html

这篇简易指南可能在后面更新,也可能不。

【END】

Comments is loading... / 评论区正在加载中...