- 构建工具
- CLI
【免费下载链接】leiningen
Moved to Codeberg; this is a temporary convenience mirror
Leiningen 的 Profiles(配置文件/配置档)机制让你可以按场景灵活切换项目配置——例如在开发期把额外的测试数据目录挂到 classpath 上而不打进 jar,或在每个项目里都注入开发工具而不改动任何project.clj。本文基于 doc/PROFILES.md 展开,结合leiningen-core源码与测试,系统讲解 Profile 的声明位置、默认行为、合并规则、激活方式与调试手段,读完即可在生产项目中熟练运用with-profile、profiles.clj与 profile 元数据。
什么是 Profile
Profile 本质上是一个包含任意defproject支持键值对的映射(map)。当某个 profile 被激活时,它的内容会被合并进项目 map(project map),从而改变项目的配置。其典型用法是:开发时需要额外的测试数据目录和测试依赖,但又不希望它们进入发布产物:
(defproject myproject "0.5.0-SNAPSHOT" :description "A project for doing things." :dependencies [[org.clojure/clojure "1.4.0"]] :profiles {:dev {:resource-paths ["dummy-data"] :dependencies [[expectations "1.4.41"]]}})上面的:devprofile 在开发期间把dummy-data目录加入资源路径,并引入测试框架expectations;因为:dev属于默认激活的 profile,它不会污染最终发布的 jar。
用show-profiles任务可以列出项目可用的全部 profile:
$ lein show-profiles带参数时则显示单个 profile 的完整内容(该任务的实现在 src/leiningen/show_profiles.clj,其无参形式会列出所有已读取 profile 的名字,带参形式打印对应 profile map)。
声明 Profile
Profile 可以在多个层级声明,其优先级(从高到低)为:项目根目录的profiles.clj>project.clj> 用户级 profile > 系统级 profile。
项目内声明:project.clj 与 profiles.clj
在project.clj中通过:profiles键声明是最常见的方式(见上面的例子)。此外,还可以在项目根目录放置profiles.clj文件,其中的 profile 会通过合并逻辑覆盖project.clj中同名 profile,因此适合存放不想提交进版本控制的项目级覆盖配置。源码层面,read-profiles会依次读取系统级、用户级、project.clj与项目根profiles.clj,并用merge让后者覆盖前者,参见 leiningen-core/src/leiningen/core/project.clj 与 project-profiles。
用户级与系统级声明
用户级 profile 放在~/.lein/profiles.clj,对所有项目生效,但会被项目内同名 profile 覆盖。系统级 profile 放在/etc/leiningen/profiles.clj(Windows 上为AllUsersProfile\Leiningen),与用户级语义相同但优先级更低。
还可以在~/.lein/profiles.d目录下放置若干.clj文件定义用户级 profile。这里的语义与其他文件略有不同:文件顶层直接就是 profile map(而不是 map of maps),profile 的名字取自文件名(去掉.clj后缀)。例如~/.lein/profiles.d/devtools.clj的内容直接写成:
{:plugins [[lein-pprint "1.1.1"]]}注意:同一个用户级 profile 若同时定义在~/.lein/profiles.clj和~/.lein/profiles.d中会被视为错误。这一行为的源码依据在 leiningen-core/src/leiningen/core/user.clj:profiles-d-profiles以文件名(去掉.clj)作为 profile 关键字,profiles函数用merge-with error-fn合并两个来源,重复定义会抛出异常并设置退出码 1。
用户级 profile 示例
如果你希望某些开发工具在每个项目里都可用,把它们放进~/.lein/profiles.clj的:userprofile:
{:user {:plugins [[lein-pprint "1.1.1"]] :dependencies [[slamhound "1.3.1"]]}}默认 Profile
不通过with-profile指定其他 profile 集合时,以下 profile 默认处于激活状态。它们各自语义不同:
:user:用户级开发工具。想在任何项目里访问的依赖或插件都放这里(见上面示例)。:dev:项目级开发工具。放构建或测试必需的东西,而不仅仅是便利工具。它与:user相互独立;为避免冲突,项目不应定义:userprofile,用户也不应定义全局:devprofile。:system:类似:user,但作用范围是整个系统而非单个用户;系统级 profile 应使用:system,同样不应定义:user或:dev。:base:Leiningen 自带的基底 profile,提供 REPL 基础功能所需的依赖、把dev-resources加入:resource-paths,并为:jvm-opts、:checkout-deps-shares、:test-selectors设置默认值。通常不需要修改它。:provided:声明"假定由运行环境提供"的依赖——这些依赖在 jar 创建时需要,但不会传递给依赖你项目的下游代码。典型场景是 Hadoop 这类自带部分库的框架。:default:指定运行lein任务时默认激活的 profile 集合;若未覆盖,其值为:leiningen/default,一个组合 profile,内容为[:base :system :user :provided :dev]。
以上默认 profile 在开发期间生效,但在生成 jar 和 pom 之前会被解除合并(unmerge),从而对依赖你项目的代码不可见。这部分默认 profile 的完整定义(包括:leiningen/default、:base、:leiningen/test、:uberjar、:update、:offline、:debug以及各 profile 的默认元数据)可查看 default-profiles 与 default-profile-metadata。
任务特定的 Profile
某些任务在运行时若检测到对应 profile 会自动合并:
test任务会自动合并:testprofile;repl任务会自动合并:replprofile。
repl任务的源码确认了这一行为:在 src/leiningen/repl.clj 中,任务通过profiles-with-matching-meta找出带:repl元数据的 profile 并执行merge-profiles;该行为也记录在lein help repl的文档串中("the :repl profile is implicitly activated for this task")。注意:testprofile 是强烈不建议使用的:把东西放进去可能导致测试无法在 REPL 中运行。
Profile 元数据
Profile 可以携带元数据(metadata)来改变其行为:
^:leaky:标记为 leaky 的 profile 在生成 pom 和 jar 时不会被剥离。其反向语义即non-leaky-profiles函数所实现的:默认情况下,非 leaky 的 profile 会在发布产物生成阶段被移除,参见 leiningen-core/src/leiningen/core/project.clj。:uberjarprofile 在默认元数据中被标记为:leaky true。^{:pom-scope :test}:profile 激活时,其:dependencies会以testscope 写入生成的 pom 和 jar。:dev、:test、:base三个 profile 已自动带此元数据。^{:pom-scope :provided}:profile 激活时,其:dependencies会以providedscope 写入 pom 和 jar。:providedprofile 已自动带此元数据。
这些默认元数据在 default-profile-metadata 中定义;将:pom-scope实际写入依赖的底层逻辑是set-dependencies-pom-scope(为每个依赖追加:scope <scope>),见 leiningen-core/src/leiningen/core/project.clj。
合并(Merging)
Profile 的合并规则是:对项目 map / profile map 中的每个键,如果值是集合则合并,否则替换。后指定的 profile 在替换时优先(与clojure.core/merge语义一致),例如:dev默认优先于:user。具体地:
- map 递归合并;
- set 用
clojure.set/union合并; - list/vector 拼接(concatenated)。
合并逻辑的源码实现是meta-merge(见 leiningen-core/src/leiningen/core/project.clj):map 递归merge-with,set 取并集,集合按元数据决定拼接方向,类型不匹配时给出警告并取右侧值。
用元数据控制合并方向
默认的合并逻辑可以通过元数据提示来覆盖:
{:profiles {:dev {:prep-tasks ^:replace ["clean" "compile"] :aliases ^:displace {"launch" "run"}}}}^:replace:当前值优先(替换其他 profile 的值);^:displace:让位给其他 profile 的值。
^:top-displace则用于在更高优先级合并时被替换(源码中的pick-prioritized与different-priority?完整实现了这套元数据判定,见 leiningen-core/src/leiningen/core/project.clj)。
一个例外::plugins和:dependencies有自定义去重逻辑——虽然它们必须以向量形式声明,但行为上更像 map(同一依赖同一时刻只能存在一个版本)。不过 replace/displace 元数据提示仍然生效。这在源码中体现为empty-dependencies携带的:reduce归约函数reduce-dep-step(以 group/artifact/classifier/extension 为唯一键去重合并,见 leiningen-core/src/leiningen/core/project.clj)。
同名 profile 不合并
记住:同一个名字的 profile 出现在多个位置时,只会选取优先级最高的那一个,不做合并。优先级从高到低为:profiles.clj、project.clj、用户级 profile、系统级 profile。
如果需要个人化覆盖某个 profile 的部分内容,可以用组合 profile 拆分公共部分与个人覆盖部分,例如:dev [:dev-common :dev-overrides]:在project.clj里只放:dev-overrides {},然后在profiles.clj中覆盖它。
用 Profile 测试多版本依赖
Profile 常用于针对不同依赖组合运行测试:
(defproject swank-clojure "1.5.0-SNAPSHOT" :description "Swank server connecting Clojure to Emacs SLIME" :dependencies [[org.clojure/clojure "1.2.1"] [clj-stacktrace "0.2.4"] [cdt "1.2.6.2"]] :profiles {:1.3 {:dependencies [[org.clojure/clojure "1.3.0"]]} :1.4 {:dependencies [[org.clojure/clojure "1.4.0-beta1"]]}})然后通过lein with-profile 1.3 test或lein with-profile 1.4 test切换 Clojure 版本。sample.project.clj也展示了类似做法(:1.4、:1.5分别引入不同 Clojure 版本),见 sample.project.clj。
激活 Profile
要为一个任务切换 profile 集合,使用with-profile这个高阶任务(higher-order task,其实现位于 src/leiningen/with_profile.clj)。
指定一个 profile:
$ lein with-profile 1.3 test :database多个 profile 用逗号组合(合并后执行一次任务):
$ lein with-profile qa,user test :database多个 profile 用冒号串联(依次执行多次任务,每次激活一组 profile;若某组失败会累计错误并在最后 abort,且会打印Performing task '...' with profile(s): '...'提示):
$ lein with-profile 1.3:1.4 test :database上面的调用是替换默认 profile 集合;若要在默认集合之上追加,用+前缀;用-前缀可以停用某些 profile:
$ lein with-profile +server,+fast runwith-profile的源码还规定了一个约束:profile 要么全部带+/-前缀,要么全部不带,混用会抛出"Profiles in with-profile must either all be qualified, or none qualified"异常(见 src/leiningen/with_profile.clj)。
关于 target-path 的建议
默认情况下所有 profile 共享同一个:target-path,这可能导致某个 profile 的设置泄漏到另一个 profile。建议把:target-path设为"target/%s",这样每个 profile 集合被隔离到独立子目录,防止相互污染:
(defproject myproject "0.5.0-SNAPSHOT" :target-path "target/%s" ...)源码中profile-scope-target-path正是用target/%s中的%s填充激活 profile 的规范化名称(并对 map 类 profile 计算 SHA1 前缀),见 leiningen-core/src/leiningen/core/project.clj。
组合 Profile(Composite Profiles)
可以把 profile 定义为其他 profile 的组合——将 profile 值写成向量而非 map,向量内放引用其他 profile 的关键字,合并时按顺序把各 profile 合并到一起。这能避免重复定义:
{:shared {:port 9229, :protocol "https"} :qa-servers {:servers ["qa.mycorp.com"]} :prod-servers {:servers ["prod1.mycorp.com", "prod1.mycorp.com"]} :qa [:shared :qa-servers] :production [:shared :prod-servers]}注意两个限制:
- 虽然目前允许组合 profile 中混入 map(源码会给出"Composite profiles containing maps are strongly recommended against"的警告,见 leiningen-core/src/leiningen/core/project.clj),但这在未来版本中会成为错误。建议把 map 拆成独立命名的顶层 profile。
- 组合 profile 无法传播某些类型的元数据,因此与
:providedprofile不兼容。如果遇到 "Composite profiles are incompatible with :provided." 错误(该异常由lookup-profile*抛出,见 leiningen-core/src/leiningen/core/project.clj),改为给包含依赖的那个 profile map 加上^{:pom-scope :provided}元数据。
动态求值(Dynamic Eval)
有时你想在 profile 中读取环境变量或执行函数来获取值。在profiles.clj中需要借助 read-eval 语法#=(eval ...):
{:user {:compile-path #=(eval (System/getenv "ci.compile-path")), :target-path #=(eval (System/getenv "ci.target-path"))}}调试 Profile
要查看某个 profile 如何影响项目 map,使用lein-pprint插件(本仓库即自带实现 lein-pprint/src/leiningen/pprint.clj,lein-pprint也是文档调试示例中使用的插件):
$ lein with-profile 1.4 pprint {:compile-path "/home/phil/src/leiningen/lein-pprint/classes", :group "lein-pprint", :source-path ("/home/phil/src/leiningen/lein-pprint/src"), :dependencies ([nrepl "0.8.3" :exclusions [org.clojure/clojure]] [incomplete "0.1.0" :exclusions [org.clojure/clojure]] [org.thnetos/cd-client "0.3.3" :exclusions [org.clojure/clojure]]), :target-path "/home/phil/src/leiningen/lein-pprint/target", :name "lein-pprint", [...] :description "Pretty-print a representation of the project map."}pprint任务支持--no-pretty --前缀以关闭美化输出,也支持传入 selector(如lein pprint :dependencies)只打印项目 map 中的指定键,参见 lein-pprint/src/leiningen/pprint.clj。
发布产物中的 Profile 处理
为了防止 profile 设置泄漏给依赖你项目的其他工程,在生成 pom、jar 和 uberjar 时,:default系列 profile 会被从项目中移除;而创建 uberjar 时,若存在:uberjarprofile 则会包含进来(这在想为 uberjar 指定:main命名空间、又不想在常规开发中触发 AOT 编译时很有用)。通过显式with-profile激活的 profile 会被保留。
这一"剥离"逻辑的源码基础是non-leaky-profiles(移除所有未带:leaky元数据的已激活 profile)与pom-scope-profiles(按:pom-scope分类返回 profile),见 leiningen-core/src/leiningen/core/project.clj;profile 元数据在测试与 dev-resources 中也有印证,例如 leiningen-core/dev-resources/profile-metadata.clj 用^:replace、^:displace元数据验证合并行为,测试用例 leiningen-core/test/leiningen/core/test/project.clj 覆盖了 profile 合并、解合并与优先级等场景。
小结
Leiningen 的 profile 体系覆盖了从用户级工具注入到项目级开发配置、从多版本依赖测试到发布产物隔离的完整需求。掌握四个要点即可熟练使用:声明位置决定优先级(profiles.clj>project.clj> 用户级 > 系统级)、合并规则默认集合拼接(map 递归、set 并集、向量拼接,必要时用^:replace/^:displace干预)、激活方式灵活(with-profile的逗号合并、冒号串联、+/-增删),以及用元数据控制发布行为(^:leaky与^{:pom-scope ...})。遇到配置不生效时,lein show-profiles和lein with-profile <name> pprint是你最趁手的两个调试工具。
- 构建工具
- CLI
【免费下载链接】leiningen
Moved to Codeberg; this is a temporary convenience mirror
相关推荐
Leiningen构建配置复用:跨项目共享profiles与模板
Leiningen构建配置复用:跨项目共享profiles与模板 在Clojure开发中,重复配置项目构建环境是影响效率的常见痛点。Leiningen(莱宁根)
构建工具CLIGraalVM Native Image PGO 配置文件合并(Merging Profiles from Multiple Sources)完全指南
GraalVM Native Image PGO 配置文件合并(Merging Profiles from Multiple Sources)完全指南 本文面向
编译器JIT编译语言运行时高性能计算内存管理TypeScript 声明合并与类型扩展:同名声明合并与接口继承的完整实战指南
TypeScript 声明合并与类型扩展:同名声明合并与接口继承的完整实战指南 导读 在 TypeScript 的类型系统中,"合并(Merging)"与"扩展
文档教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考