4. 从 pom.xml 提取依赖许可证信息
为什么 pom.xml 本身不够
Maven 的 pom.xml 只声明直接依赖,且 <licenses> 是可选字段,很多构件根本不写。真正决定合规风险的是解析后的完整依赖树:同一 groupId:artifactId 可能因不同路径引入多个版本,最终只有一个进入 classpath。因此提取流程必须分两步:先用 Maven 解析出有效依赖集合,再逐个读取构件元数据中的许可证。
用 Maven 命令拿到有效依赖清单
不要手工递归解析 pom,直接让 Maven 输出解析后的结果。以下命令生成包含 groupId、artifactId、version、scope 的列表:
mvn dependency:list -DoutputFile=deps.txt -DincludeScope=runtime -DoutputAbsoluteArtifactFilename=false
输出格式为 groupId:artifactId:type:version:scope。若需要树形结构排查冲突来源,改用:
mvn dependency:tree -DoutputFile=tree.txt -Dverbose
-Dverbose 会显示被省略的冲突版本,便于确认仲裁结果。
从构件元数据提取 licenses
每个已解析构件在本地仓库中都有对应的 .pom 文件,路径为:
~/.m2/repository/{groupId路径}/{artifactId}/{version}/{artifactId}-{version}.pom
其中 <licenses> 块结构如下:
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
<distribution>repo</distribution>
</license>
</licenses>
注意:<name> 是自由文本,不是 SPDX 标识符。例如 "Apache License, Version 2.0" 需要映射为 Apache-2.0,"The MIT License" 映射为 MIT。映射表应作为独立配置维护。
Python 提取脚本
以下脚本读取 deps.txt,定位每个构件的 pom 文件,解析 licenses 并输出 CSV:
import csv, os, re
from xml.etree import ElementTree as ET
M2 = os.path.expanduser("~/.m2/repository")
NS = {"m": "http://maven.apache.org/POM/4.0.0"}
# 自由文本 → SPDX 标识符映射(按需扩充)
SPDX_MAP = {
"Apache License, Version 2.0": "Apache-2.0",
"The Apache Software License, Version 2.0": "Apache-2.0",
"The MIT License": "MIT",
"MIT License": "MIT",
"Eclipse Public License - v 1.0": "EPL-1.0",
"Eclipse Public License v2.0": "EPL-2.0",
"GNU Lesser General Public License": "LGPL-2.1-only",
}
def pom_path(g, a, v):
return os.path.join(M2, *g.split("."), a, v, f"{a}-{v}.pom")
def extract_licenses(path):
if not os.path.isfile(path):
return [], "POM_NOT_FOUND"
try:
root = ET.parse(path).getroot()
except ET.ParseError:
return [], "POM_PARSE_ERROR"
lics = []
for lic in root.findall(".//m:licenses/m:license", NS):
name = (lic.findtext("m:name", "", NS) or "").strip()
url = (lic.findtext("m:url", "", NS) or "").strip()
if name:
lics.append({"name": name, "url": url,
"spdx": SPDX_MAP.get(name, "UNMAPPED")})
return lics, ("OK" if lics else "NO_LICENSE")
rows = []
with open("deps.txt", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line or line.startswith("["):
continue
parts = line.split(":")
if len(parts) < 5:
continue
g, a, _, v, scope = parts[0], parts[1], parts[2], parts[3], parts[4]
lics, status = extract_licenses(pom_path(g, a, v))
if lics:
for l in lics:
rows.append([g, a, v, scope, l["name"], l["spdx"], l["url"], status])
else:
rows.append([g, a, v, scope, "", "UNKNOWN", "", status])
with open("license_report.csv", "w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(["groupId","artifactId","version","scope",
"license_name","spdx_id","license_url","status"])
w.writerows(rows)
print(f"共 {len(rows)} 条记录,已写入 license_report.csv")
结果判读
| status | 含义 | 处理建议 |
|---|---|---|
| OK | pom 中声明了许可证 | 检查 spdx_id 是否为 UNMAPPED,若是则补充映射 |
| NO_LICENSE | pom 存在但无 licenses 块 | 访问构件主页或源码仓库查找 LICENSE 文件 |
| POM_NOT_FOUND | 本地仓库无对应 pom | 先执行 mvn dependency:resolve 下载 |
| POM_PARSE_ERROR | pom 格式异常 | 手动检查该文件,可能是损坏或非标准 pom |
pom 中的 licenses 由构件作者填写,可能不准确或过时。对于高风险许可证(GPL、AGPL)或 UNKNOWN 状态,必须回溯到构件源码仓库中的 LICENSE 文件确认,不能仅凭 pom 声明下结论。
若项目使用多模块聚合 pom,在根目录执行
mvn dependency:list 会汇总所有子模块的依赖。如需按模块拆分,可在每个子模块目录单独执行,或在输出中保留模块路径信息。