已合并
fix: 代码检视修复 #116
IceT_5创建于 8月7日
fix: 代码检视修复 #116
已合并
IceT_5创建于 8月7日
8 个文件变更+273-279
@@ -37,7 +37,6 @@ cret -t <target> -n <software_name> -v <software_version> [可选参数]
37| `-o` | `--output-dir` | 输出目录,默认为被测目标同级目录 |37| `-o` | `--output-dir` | 输出目录,默认为被测目标同级目录 |
38| `-j` | `--jobs` | ScanCode并行进程数,默认4(内存不足时可降低) |38| `-j` | `--jobs` | ScanCode并行进程数,默认4(内存不足时可降低) |
39| | `--max-in-memory` | ScanCode内存缓存文件数,默认2000;设为0表示不限制内存使用(禁用磁盘缓存);设为-1表示仅使用磁盘缓存(最小化内存占用) |39| | `--max-in-memory` | ScanCode内存缓存文件数,默认2000;设为0表示不限制内存使用(禁用磁盘缓存);设为-1表示仅使用磁盘缓存(最小化内存占用) |
40-| | `--timeout` | 单文件扫描超时时间(秒),默认60(超时文件将被跳过) |
41| `--guide` | | 显示详细使用指南 |40| `--guide` | | 显示详细使用指南 |
42 41 
43### ⚠️ 重要提示:License名称必须使用SPDX标识符42### ⚠️ 重要提示:License名称必须使用SPDX标识符
@@ -67,7 +66,7 @@ cret -t package.zip -n "MySoftware" -v "1.0.0" -l "MIT" -p "LICENSE"
67cret -t package.zip -n "MySoftware" -v "1.0.0" -o "./output"66cret -t package.zip -n "MySoftware" -v "1.0.0" -o "./output"
68 67 
69# 调整ScanCode性能参数(适用于大项目或内存有限的机器)68# 调整ScanCode性能参数(适用于大项目或内存有限的机器)
70-cret -t package.zip -n "MySoftware" -v "1.0.0" -j 2 --max-in-memory 1000 --timeout 3069+cret -t package.zip -n "MySoftware" -v "1.0.0" -j 2 --max-in-memory 1000
71 70 
72# 显示详细使用指南71# 显示详细使用指南
73cret --guide72cret --guide
@@ -375,4 +374,3 @@ OSSinfo_extraction/
375## 许可证374## 许可证
376 375 
377本项目采用 Apache License 2.0 许可证。376本项目采用 Apache License 2.0 许可证。
378- 
@@ -28,121 +28,121 @@ LICENSE_FILE_PATTERNS = [
28 r"^copying[-_.].*", # COPYING.LESSER, COPYING.GPL, COPYING-3.028 r"^copying[-_.].*", # COPYING.LESSER, COPYING.GPL, COPYING-3.0
29 r"^copyright$", # COPYRIGHT, copyright29 r"^copyright$", # COPYRIGHT, copyright
30 r"^copyright[-_.].*", # COPYRIGHT.txt, COPYRIGHT.md30 r"^copyright[-_.].*", # COPYRIGHT.txt, COPYRIGHT.md
31- 31+ 
32 # 按许可证名称匹配(支持SPDX标识符格式)32 # 按许可证名称匹配(支持SPDX标识符格式)
33 # MIT 系列33 # MIT 系列
34 r"^mit$", # MIT.txt34 r"^mit$", # MIT.txt
35 r"^mit[-_.].*", # MIT-License, MIT.txt, MIT-035 r"^mit[-_.].*", # MIT-License, MIT.txt, MIT-0
36- 36+ 
37 # Apache 系列37 # Apache 系列
38 r"^apache$", # Apache.txt38 r"^apache$", # Apache.txt
39 r"^apache[-_.].*", # Apache-2.0, Apache-License, Apache.txt39 r"^apache[-_.].*", # Apache-2.0, Apache-License, Apache.txt
40- 40+ 
41 # BSD 系列41 # BSD 系列
42 r"^bsd$", # BSD.txt42 r"^bsd$", # BSD.txt
43 r"^bsd[-_.].*", # BSD-2-Clause, BSD-3-Clause, BSD-4-Clause, BSD.txt43 r"^bsd[-_.].*", # BSD-2-Clause, BSD-3-Clause, BSD-4-Clause, BSD.txt
44- 44+ 
45 # GPL 系列45 # GPL 系列
46 r"^gpl$", # GPL.txt46 r"^gpl$", # GPL.txt
47 r"^gpl[-_.].*", # GPL-3.0, GPL-2.0, GPL-3.0-only, GPL-3.0-or-later47 r"^gpl[-_.].*", # GPL-3.0, GPL-2.0, GPL-3.0-only, GPL-3.0-or-later
48- 48+ 
49 # LGPL 系列49 # LGPL 系列
50 r"^lgpl$", # LGPL.txt50 r"^lgpl$", # LGPL.txt
51 r"^lgpl[-_.].*", # LGPL-3.0, LGPL-2.1, LGPL.txt51 r"^lgpl[-_.].*", # LGPL-3.0, LGPL-2.1, LGPL.txt
52- 52+ 
53 # AGPL 系列53 # AGPL 系列
54 r"^agpl$", # AGPL.txt54 r"^agpl$", # AGPL.txt
55 r"^agpl[-_.].*", # AGPL-3.0, AGPL-3.0-only55 r"^agpl[-_.].*", # AGPL-3.0, AGPL-3.0-only
56- 56+ 
57 # MPL 系列57 # MPL 系列
58 r"^mpl$", # MPL.txt58 r"^mpl$", # MPL.txt
59 r"^mpl[-_.].*", # MPL-2.0, MPL.txt59 r"^mpl[-_.].*", # MPL-2.0, MPL.txt
60- 60+ 
61 # EPL (Eclipse Public License) 系列61 # EPL (Eclipse Public License) 系列
62 r"^epl$", # EPL.txt62 r"^epl$", # EPL.txt
63 r"^epl[-_.].*", # EPL-1.0, EPL-2.0, EPL.txt63 r"^epl[-_.].*", # EPL-1.0, EPL-2.0, EPL.txt
64- 64+ 
65 # Creative Commons 系列65 # Creative Commons 系列
66 r"^cc0$", # CC0.txt66 r"^cc0$", # CC0.txt
67 r"^cc0[-_.].*", # CC0-1.0, CC0.txt67 r"^cc0[-_.].*", # CC0-1.0, CC0.txt
68 r"^cc[-_.].*", # CC-BY, CC-BY-SA, CC-BY-4.068 r"^cc[-_.].*", # CC-BY, CC-BY-SA, CC-BY-4.0
69- 69+ 
70 # ISC License70 # ISC License
71 r"^isc$", # ISC.txt71 r"^isc$", # ISC.txt
72 r"^isc[-_.].*", # ISC-License72 r"^isc[-_.].*", # ISC-License
73- 73+ 
74 # Unlicense74 # Unlicense
75 r"^unlicense$", # UNLICENSE75 r"^unlicense$", # UNLICENSE
76 r"^unlicense[-_.].*", # UNLICENSE.txt76 r"^unlicense[-_.].*", # UNLICENSE.txt
77- 77+ 
78 # zlib License78 # zlib License
79 r"^zlib$", # zlib.txt79 r"^zlib$", # zlib.txt
80 r"^zlib[-_.].*", # zlib-license80 r"^zlib[-_.].*", # zlib-license
81- 81+ 
82 # PostgreSQL License82 # PostgreSQL License
83 r"^postgresql$", # PostgreSQL.txt83 r"^postgresql$", # PostgreSQL.txt
84 r"^postgresql[-_.].*", # PostgreSQL-License84 r"^postgresql[-_.].*", # PostgreSQL-License
85- 85+ 
86 # Open Font License86 # Open Font License
87 r"^ofl$", # OFL87 r"^ofl$", # OFL
88 r"^ofl[-_.].*", # OFL.txt, OFL-1.188 r"^ofl[-_.].*", # OFL.txt, OFL-1.1
89- 89+ 
90 # Artistic License90 # Artistic License
91 r"^artistic$", # Artistic.txt91 r"^artistic$", # Artistic.txt
92 r"^artistic[-_.].*", # Artistic-2.0, Artistic-License92 r"^artistic[-_.].*", # Artistic-2.0, Artistic-License
93- 93+ 
94 # 0BSD (Zero-Clause BSD)94 # 0BSD (Zero-Clause BSD)
95 r"^0bsd$", # 0BSD.txt95 r"^0bsd$", # 0BSD.txt
96- 96+ 
97 # Boost Software License97 # Boost Software License
98 r"^bsl$", # BSL.txt98 r"^bsl$", # BSL.txt
99 r"^bsl[-_.].*", # BSL-1.099 r"^bsl[-_.].*", # BSL-1.0
100 r"^boost[-_.].*", # Boost-1.0, Boost-License100 r"^boost[-_.].*", # Boost-1.0, Boost-License
101- 101+ 
102 # EUPL (European Union Public License)102 # EUPL (European Union Public License)
103 r"^eupl$", # EUPL.txt103 r"^eupl$", # EUPL.txt
104 r"^eupl[-_.].*", # EUPL-1.1, EUPL-1.2104 r"^eupl[-_.].*", # EUPL-1.1, EUPL-1.2
105- 105+ 
106 # CDDL (Common Development and Distribution License)106 # CDDL (Common Development and Distribution License)
107 r"^cddl$", # CDDL.txt107 r"^cddl$", # CDDL.txt
108 r"^cddl[-_.].*", # CDDL-1.0, CDDL-1.1108 r"^cddl[-_.].*", # CDDL-1.0, CDDL-1.1
109- 109+ 
110 # Eclipse Distribution License110 # Eclipse Distribution License
111 r"^edl$", # EDL.txt111 r"^edl$", # EDL.txt
112 r"^edl[-_.].*", # EDL-1.0112 r"^edl[-_.].*", # EDL-1.0
113- 113+ 
114 # LaTeX Project Public License114 # LaTeX Project Public License
115 r"^lppl$", # LPPL.txt115 r"^lppl$", # LPPL.txt
116 r"^lppl[-_.].*", # LPPL-1.3c116 r"^lppl[-_.].*", # LPPL-1.3c
117- 117+ 
118 # Microsoft Public License118 # Microsoft Public License
119 r"^ms[-_]?pl$", # MS-PL, MSPL119 r"^ms[-_]?pl$", # MS-PL, MSPL
120- 120+ 
121 # Mozilla Public License (alternate naming)121 # Mozilla Public License (alternate naming)
122 r"^mozilla[-_.].*", # Mozilla-Public-License122 r"^mozilla[-_.].*", # Mozilla-Public-License
123- 123+ 
124 # OpenSSL License124 # OpenSSL License
125 r"^openssl$", # OpenSSL.txt125 r"^openssl$", # OpenSSL.txt
126- 126+ 
127 # PHP License127 # PHP License
128 r"^php[-_.].*", # PHP-3.0, PHP-License128 r"^php[-_.].*", # PHP-3.0, PHP-License
129- 129+ 
130 # Python License130 # Python License
131 r"^python[-_.].*", # Python-2.0, Python-License131 r"^python[-_.].*", # Python-2.0, Python-License
132- 132+ 
133 # SIL Open Font License (alternate naming)133 # SIL Open Font License (alternate naming)
134 r"^sil[-_.].*", # SIL-OFL-1.1134 r"^sil[-_.].*", # SIL-OFL-1.1
135- 135+ 
136 # Vim License136 # Vim License
137 r"^vim$", # Vim.txt137 r"^vim$", # Vim.txt
138- 138+ 
139 # W3C License139 # W3C License
140 r"^w3c$", # W3C.txt140 r"^w3c$", # W3C.txt
141 r"^w3c[-_.].*", # W3C-License141 r"^w3c[-_.].*", # W3C-License
142- 142+ 
143 # WTFPL (Do What The Fuck You Want To Public License)143 # WTFPL (Do What The Fuck You Want To Public License)
144 r"^wtfpl$", # WTFPL.txt144 r"^wtfpl$", # WTFPL.txt
145- 145+ 
146 # Zope Public License146 # Zope Public License
147 r"^zpl$", # ZPL.txt147 r"^zpl$", # ZPL.txt
148 r"^zpl[-_.].*", # ZPL-2.1148 r"^zpl[-_.].*", # ZPL-2.1
@@ -153,10 +153,10 @@ LICENSE_EXTENSIONS = {
153 "", ".txt", ".md", ".rst", ".html", ".htm",153 "", ".txt", ".md", ".rst", ".html", ".htm",
154 ".xml", ".json", ".yaml", ".yml", ".asciidoc",154 ".xml", ".json", ".yaml", ".yml", ".asciidoc",
155 ".adoc", ".markdown", ".license", ".header",155 ".adoc", ".markdown", ".license", ".header",
156- ".lesser", ".gpl", ".GPL", ".apache", ".APACHE",156+ ".lesser", ".gpl", ".apache",
157- ".mit", ".MIT", ".bsd", ".BSD", ".lgpl", ".LGPL",157+ ".mit", ".bsd", ".lgpl",
158- ".mpl", ".MPL", ".ofl", ".OFL", ".unlicense",158+ ".mpl", ".ofl", ".unlicense",
159- ".artistic", ".cc0", ".CC0",159+ ".artistic", ".cc0",
160}160}
161 161 
162# Copyright 提取时忽略的文件扩展名(文档类文件)162# Copyright 提取时忽略的文件扩展名(文档类文件)
@@ -43,22 +43,24 @@ def run_extractcode(target: str) -> Path:
43 info("Running extractcode:")43 info("Running extractcode:")
44 log_cmd(" ".join(cmd))44 log_cmd(" ".join(cmd))
45 45 
46- result = subprocess.run(46+ try:
47- cmd,47+ result = subprocess.run(
48- stdout=sys.stdout,48+ cmd,
49- stderr=sys.stderr,49+ stdout=sys.stdout,
50- env=env,50+ stderr=sys.stderr,
51- )51+ env=env,
52- 
53- if result.returncode != 0:
54- raise RuntimeError(f"extractcode failed with exit code {result.returncode}")
55- 
56- extract_dir = archive.parent / f"{archive.name}-extract"
57- 
58- if not extract_dir.exists():
59- raise RuntimeError(
60- f"Expected extract directory not found: {extract_dir}"
61 )52 )
62- 53+ 
63- shutil.rmtree(custom_tmp)54+ if result.returncode != 0:
55+ raise RuntimeError(f"extractcode failed with exit code {result.returncode}")
56+ 
57+ extract_dir = archive.parent / f"{archive.name}-extract"
58+ 
59+ if not extract_dir.exists():
60+ raise RuntimeError(
61+ f"Expected extract directory not found: {extract_dir}"
62+ )
63+ finally:
64+ shutil.rmtree(custom_tmp, ignore_errors=True)
65+ 
64 return extract_dir66 return extract_dir
@@ -12,7 +12,6 @@
12# See the License for the specific language governing permissions and12# See the License for the specific language governing permissions and
13# limitations under the License.13# limitations under the License.
14 14 
15-import json
16import re15import re
17from pathlib import Path16from pathlib import Path
18from typing import Set, List, Dict, Any, Optional17from typing import Set, List, Dict, Any, Optional
@@ -24,19 +23,19 @@ from .config import LICENSE_FILE_PATTERNS, LICENSE_EXTENSIONS
24def _is_exact_license_keyword(stem: str) -> bool:23def _is_exact_license_keyword(stem: str) -> bool:
25 """24 """
26 判断stem是否精确匹配license关键词(不带后缀修饰)。25 判断stem是否精确匹配license关键词(不带后缀修饰)。
27- 26+ 
28 当stem精确匹配 license/copying/copyright 等关键词时,27 当stem精确匹配 license/copying/copyright 等关键词时,
29 文件名中的后缀部分(如 COPYING.GPL2 中的 .GPL2)是license类型限定符,28 文件名中的后缀部分(如 COPYING.GPL2 中的 .GPL2)是license类型限定符,
30 而非文件格式扩展名,因此应跳过扩展名白名单检查。29 而非文件格式扩展名,因此应跳过扩展名白名单检查。
31- 30+ 
32 例如:31 例如:
33 - COPYING.GPL2 → stem='copying'True(.GPL2是license类型限定符)32 - COPYING.GPL2 → stem='copying'True(.GPL2是license类型限定符)
34 - COPYING.md → stem='copying'True(.md虽然也是扩展名,但不应阻止匹配)33 - COPYING.md → stem='copying'True(.md虽然也是扩展名,但不应阻止匹配)
35 - license_1_0 → stem='license_1_0'False(有后缀修饰,走正常扩展名检查)34 - license_1_0 → stem='license_1_0'False(有后缀修饰,走正常扩展名检查)
36- 35+ 
37 Args:36 Args:
38 stem: 文件名(不含扩展名)的小写形式37 stem: 文件名(不含扩展名)的小写形式
39- 38+ 
40 Returns:39 Returns:
41 bool: 是否是精确的license关键词40 bool: 是否是精确的license关键词
42 """41 """
@@ -54,54 +53,54 @@ def _is_exact_license_keyword(stem: str) -> bool:
54def is_license_file(filename: str) -> bool:53def is_license_file(filename: str) -> bool:
55 """54 """
56 判断文件名是否是license相关文件。55 判断文件名是否是license相关文件。
57- 56+ 
58 检查规则:57 检查规则:
59 1. 文件名(不含扩展名)必须匹配已知的license模式58 1. 文件名(不含扩展名)必须匹配已知的license模式
60 2. 扩展名必须在允许列表中(但当stem精确匹配license关键词时跳过此检查,59 2. 扩展名必须在允许列表中(但当stem精确匹配license关键词时跳过此检查,
61 因为此时后缀是license类型限定符而非文件格式扩展名)60 因为此时后缀是license类型限定符而非文件格式扩展名)
62- 61+ 
63 注意:此函数仅检查文件名是否符合规范,不检查路径。62 注意:此函数仅检查文件名是否符合规范,不检查路径。
64 对于LICENSES目录下命名不规范的文件,由_is_valid_license_path函数处理。63 对于LICENSES目录下命名不规范的文件,由_is_valid_license_path函数处理。
65- 64+ 
66 Args:65 Args:
67 filename: 文件名(不含路径)66 filename: 文件名(不含路径)
68- 67+ 
69 Returns:68 Returns:
70 bool: 是否是license文件69 bool: 是否是license文件
71 """70 """
72 # 获取不含扩展名的文件名71 # 获取不含扩展名的文件名
73 stem = Path(filename).stem.lower()72 stem = Path(filename).stem.lower()
74 ext = Path(filename).suffix.lower()73 ext = Path(filename).suffix.lower()
75- 74+ 
76 # 检查文件名是否匹配license模式75 # 检查文件名是否匹配license模式
77 matched_pattern = None76 matched_pattern = None
78 for pattern in LICENSE_FILE_PATTERNS:77 for pattern in LICENSE_FILE_PATTERNS:
79 if re.match(pattern, stem, re.IGNORECASE):78 if re.match(pattern, stem, re.IGNORECASE):
80 matched_pattern = pattern79 matched_pattern = pattern
81 break80 break
82- 81+ 
83 if not matched_pattern:82 if not matched_pattern:
84 return False83 return False
85- 84+ 
86 # 当stem精确匹配license关键词(如 COPYING.GPL2 中的 copying)时,85 # 当stem精确匹配license关键词(如 COPYING.GPL2 中的 copying)时,
87 # 后缀部分是license类型限定符而非文件格式扩展名,跳过扩展名检查86 # 后缀部分是license类型限定符而非文件格式扩展名,跳过扩展名检查
88 if _is_exact_license_keyword(stem):87 if _is_exact_license_keyword(stem):
89 return True88 return True
90- 89+ 
91 # 其他情况:检查扩展名是否在允许列表中(空扩展名始终允许)90 # 其他情况:检查扩展名是否在允许列表中(空扩展名始终允许)
92 if ext and ext not in LICENSE_EXTENSIONS:91 if ext and ext not in LICENSE_EXTENSIONS:
93 return False92 return False
94- 93+ 
95 return True94 return True
96 95 
97 96 
98def is_license_directory(dirname: str) -> bool:97def is_license_directory(dirname: str) -> bool:
99 """98 """
100 判断目录名是否包含license字样。99 判断目录名是否包含license字样。
101- 100+ 
102 Args:101 Args:
103 dirname: 目录名102 dirname: 目录名
104- 103+ 
105 Returns:104 Returns:
106 bool: 是否是license相关目录105 bool: 是否是license相关目录
107 """106 """
@@ -112,21 +111,21 @@ def is_license_directory(dirname: str) -> bool:
112def get_license_files_from_root(root_path: Path) -> Set[str]:111def get_license_files_from_root(root_path: Path) -> Set[str]:
113 """112 """
114 获取项目根目录下的license相关文件的相对路径。113 获取项目根目录下的license相关文件的相对路径。
115- 114+ 
116 root_path 已经是真正的项目根目录(在main.py中已处理嵌套目录检测)。115 root_path 已经是真正的项目根目录(在main.py中已处理嵌套目录检测)。
117 返回相对于root_path的相对路径字符串集合。116 返回相对于root_path的相对路径字符串集合。
118- 117+ 
119 Args:118 Args:
120 root_path: 项目根目录路径119 root_path: 项目根目录路径
121- 120+ 
122 Returns:121 Returns:
123 Set[str]: license相关文件的相对路径集合122 Set[str]: license相关文件的相对路径集合
124 """123 """
125 license_files = set()124 license_files = set()
126- 125+ 
127 if not root_path.is_dir():126 if not root_path.is_dir():
128 return license_files127 return license_files
129- 128+ 
130 try:129 try:
131 for item in root_path.iterdir():130 for item in root_path.iterdir():
132 if item.is_file():131 if item.is_file():
@@ -143,7 +142,7 @@ def get_license_files_from_root(root_path: Path) -> Set[str]:
143 license_files.add(str(rel_path).replace("\\", "/"))142 license_files.add(str(rel_path).replace("\\", "/"))
144 except PermissionError:143 except PermissionError:
145 warn(f"Permission denied when accessing: {root_path}")144 warn(f"Permission denied when accessing: {root_path}")
146- 145+ 
147 return license_files146 return license_files
148 147 
149 148 
@@ -155,18 +154,18 @@ def process_license_params(
155) -> List[Dict[str, Any]]:154) -> List[Dict[str, Any]]:
156 """155 """
157 根据用户提供的参数处理license信息。156 根据用户提供的参数处理license信息。
158- 157+ 
159 参数组合逻辑:158 参数组合逻辑:
160 - 只填license名称:提取与该名称对应的license文本路径159 - 只填license名称:提取与该名称对应的license文本路径
161 - 只填license路径:提取该路径对应的license名称160 - 只填license路径:提取该路径对应的license名称
162 - 都不填:执行完整license提取161 - 都不填:执行完整license提取
163- 162+ 
164 Args:163 Args:
165 data: scancode输出的JSON数据(已解析的字典)164 data: scancode输出的JSON数据(已解析的字典)
166 root_path: 项目根目录路径165 root_path: 项目根目录路径
167 license_name: 用户提供的license名称(可选)166 license_name: 用户提供的license名称(可选)
168 license_path: 用户提供的license文本相对路径(可选)167 license_path: 用户提供的license文本相对路径(可选)
169- 168+ 
170 Returns:169 Returns:
171 List[Dict]: 提取的license信息列表170 List[Dict]: 提取的license信息列表
172 """171 """
@@ -174,28 +173,28 @@ def process_license_params(
174 scanned_root = data.get("headers", [{}])[0].get("options", {}).get("input", [None])[0]173 scanned_root = data.get("headers", [{}])[0].get("options", {}).get("input", [None])[0]
175 if root_path is None:174 if root_path is None:
176 root_path = Path(scanned_root) if scanned_root else None175 root_path = Path(scanned_root) if scanned_root else None
177- 176+ 
178 # 确定要检查的license文件集合177 # 确定要检查的license文件集合
179 target_license_files = set()178 target_license_files = set()
180 if root_path:179 if root_path:
181 target_license_files = get_license_files_from_root(root_path)180 target_license_files = get_license_files_from_root(root_path)
182- 181+ 
183 # 构建目标license文件的文件名集合182 # 构建目标license文件的文件名集合
184 target_license_filenames = set()183 target_license_filenames = set()
185 for f in target_license_files:184 for f in target_license_files:
186 filename = Path(f).name.lower()185 filename = Path(f).name.lower()
187 target_license_filenames.add(filename)186 target_license_filenames.add(filename)
188- 187+ 
189 license_records = []188 license_records = []
190- 189+ 
191 if license_name and not license_path:190 if license_name and not license_path:
192 # 只填license名称:提取对应的license文本路径191 # 只填license名称:提取对应的license文本路径
193 info(f"Searching for license path matching name: {license_name}")192 info(f"Searching for license path matching name: {license_name}")
194- 193+ 
195 matching_records = _find_license_by_name(194 matching_records = _find_license_by_name(
196 data, target_license_files, target_license_filenames, license_name195 data, target_license_files, target_license_filenames, license_name
197 )196 )
198- 197+ 
199 if matching_records:198 if matching_records:
200 license_records = matching_records199 license_records = matching_records
201 info(f"Found {len(matching_records)} matching license record(s)")200 info(f"Found {len(matching_records)} matching license record(s)")
@@ -210,20 +209,20 @@ def process_license_params(
210 "license_expression": license_name,209 "license_expression": license_name,
211 "matched_text": "",210 "matched_text": "",
212 }]211 }]
213- 212+ 
214 # 提示用户人工核对213 # 提示用户人工核对
215 warn("请人工核对许可证信息是否准确 / Please manually verify the license information is accurate")214 warn("请人工核对许可证信息是否准确 / Please manually verify the license information is accurate")
216- 215+ 
217 elif license_path and not license_name:216 elif license_path and not license_name:
218 # 只填license路径:提取该路径对应的license名称217 # 只填license路径:提取该路径对应的license名称
219 info(f"Extracting license name from path: {license_path}")218 info(f"Extracting license name from path: {license_path}")
220- 219+ 
221 # 路径验证已在main.py中完成,这里不再重复验证220 # 路径验证已在main.py中完成,这里不再重复验证
222- 221+ 
223 matching_record = _find_license_by_path(222 matching_record = _find_license_by_path(
224 data, target_license_files, license_path223 data, target_license_files, license_path
225 )224 )
226- 225+ 
227 if matching_record:226 if matching_record:
228 license_records = [matching_record]227 license_records = [matching_record]
229 info(f"Found license: {matching_record['spdx_identifier']}")228 info(f"Found license: {matching_record['spdx_identifier']}")
@@ -239,37 +238,37 @@ def process_license_params(
239 "license_expression": "Unknown",238 "license_expression": "Unknown",
240 "matched_text": "",239 "matched_text": "",
241 }]240 }]
242- 241+ 
243 # 提示用户人工核对242 # 提示用户人工核对
244 warn("请人工核对许可证信息是否准确 / Please manually verify the license information is accurate")243 warn("请人工核对许可证信息是否准确 / Please manually verify the license information is accurate")
245- 244+ 
246 else:245 else:
247 # 都填或都不填的情况246 # 都填或都不填的情况
248 if license_name and license_path:247 if license_name and license_path:
249 # 同时传入license名称和license文件路径:校验是否匹配248 # 同时传入license名称和license文件路径:校验是否匹配
250 info(f"Validating license name '{license_name}' against file path '{license_path}'")249 info(f"Validating license name '{license_name}' against file path '{license_path}'")
251- 250+ 
252 # 从路径提取license信息251 # 从路径提取license信息
253 matching_record = _find_license_by_path(252 matching_record = _find_license_by_path(
254 data, target_license_files, license_path253 data, target_license_files, license_path
255 )254 )
256- 255+ 
257 if matching_record:256 if matching_record:
258 detected_spdx = matching_record.get("spdx_identifier", "")257 detected_spdx = matching_record.get("spdx_identifier", "")
259 detected_license = matching_record.get("license_expression", "")258 detected_license = matching_record.get("license_expression", "")
260- 259+ 
261 # 检查是否匹配260 # 检查是否匹配
262 license_name_lower = license_name.lower()261 license_name_lower = license_name.lower()
263 spdx_lower = detected_spdx.lower()262 spdx_lower = detected_spdx.lower()
264 license_lower = detected_license.lower()263 license_lower = detected_license.lower()
265- 264+ 
266 is_match = (265 is_match = (
267 license_name_lower == spdx_lower or266 license_name_lower == spdx_lower or
268 license_name_lower == license_lower or267 license_name_lower == license_lower or
269 license_name_lower in spdx_lower or268 license_name_lower in spdx_lower or
270 license_name_lower in license_lower269 license_name_lower in license_lower
271 )270 )
272- 271+ 
273 if is_match:272 if is_match:
274 info(f"License name '{license_name}' matches the license detected in file '{license_path}'")273 info(f"License name '{license_name}' matches the license detected in file '{license_path}'")
275 license_records = [matching_record]274 license_records = [matching_record]
@@ -295,7 +294,7 @@ def process_license_params(
295 "license_expression": license_name,294 "license_expression": license_name,
296 "matched_text": "",295 "matched_text": "",
297 }]296 }]
298- 297+ 
299 # 提示用户人工核对298 # 提示用户人工核对
300 warn("请人工核对许可证信息是否准确 / Please manually verify the license information is accurate")299 warn("请人工核对许可证信息是否准确 / Please manually verify the license information is accurate")
301 else:300 else:
@@ -304,46 +303,46 @@ def process_license_params(
304 license_records = _extract_all_licenses(303 license_records = _extract_all_licenses(
305 data, target_license_files, target_license_filenames304 data, target_license_files, target_license_filenames
306 )305 )
307- 306+ 
308 # 提示用户人工核对307 # 提示用户人工核对
309 warn("请人工核对许可证信息是否准确 / Please manually verify the license information is accurate")308 warn("请人工核对许可证信息是否准确 / Please manually verify the license information is accurate")
310- 309+ 
311 return license_records310 return license_records
312 311 
313 312 
314def _get_best_matched_text(file_info: dict) -> str:313def _get_best_matched_text(file_info: dict) -> str:
315 """314 """
316 从license_detections中获取最佳的matched_text。315 从license_detections中获取最佳的matched_text。
317- 316+ 
318 优先级规则:317 优先级规则:
319 1. 优先选择 .LICENSE 规则(完整license文本匹配)318 1. 优先选择 .LICENSE 规则(完整license文本匹配)
320 2. 其次选择 matched_length 最长的匹配319 2. 其次选择 matched_length 最长的匹配
321- 320+ 
322 注意:scancode JSON中可能没有matched_text字段,此时返回空字符串。321 注意:scancode JSON中可能没有matched_text字段,此时返回空字符串。
323 调用者应检查返回值是否为空,如果为空则需要从源文件读取。322 调用者应检查返回值是否为空,如果为空则需要从源文件读取。
324- 323+ 
325 Args:324 Args:
326 file_info: scancode文件信息325 file_info: scancode文件信息
327- 326+ 
328 Returns:327 Returns:
329 str: 最佳的matched_text,如果没有则返回空字符串328 str: 最佳的matched_text,如果没有则返回空字符串
330 """329 """
331 license_detections = file_info.get("license_detections", [])330 license_detections = file_info.get("license_detections", [])
332 if not license_detections:331 if not license_detections:
333 return ""332 return ""
334- 333+ 
335 best_text = ""334 best_text = ""
336 best_length = 0335 best_length = 0
337 best_match = None336 best_match = None
338 has_license_rule = False337 has_license_rule = False
339- 338+ 
340 for detection in license_detections:339 for detection in license_detections:
341 matches = detection.get("matches", [])340 matches = detection.get("matches", [])
342 for match in matches:341 for match in matches:
343 rule_id = match.get("rule_identifier", "")342 rule_id = match.get("rule_identifier", "")
344 matched_text = match.get("matched_text", "")343 matched_text = match.get("matched_text", "")
345 matched_length = match.get("matched_length", len(matched_text) if matched_text else 0)344 matched_length = match.get("matched_length", len(matched_text) if matched_text else 0)
346- 345+ 
347 # 优先选择 .LICENSE 规则346 # 优先选择 .LICENSE 规则
348 if rule_id.endswith(".LICENSE"):347 if rule_id.endswith(".LICENSE"):
349 if not has_license_rule or matched_length > best_length:348 if not has_license_rule or matched_length > best_length:
@@ -357,7 +356,7 @@ def _get_best_matched_text(file_info: dict) -> str:
357 best_text = matched_text356 best_text = matched_text
358 best_length = matched_length357 best_length = matched_length
359 best_match = match358 best_match = match
360- 359+ 
361 # 如果matched_text为空但有matched_length,说明JSON中没有存储matched_text360 # 如果matched_text为空但有matched_length,说明JSON中没有存储matched_text
362 # 此时返回空字符串,调用者需要从源文件读取361 # 此时返回空字符串,调用者需要从源文件读取
363 return best_text362 return best_text
@@ -366,12 +365,12 @@ def _get_best_matched_text(file_info: dict) -> str:
366def get_best_match_info(file_info: dict) -> Dict[str, Any]:365def get_best_match_info(file_info: dict) -> Dict[str, Any]:
367 """366 """
368 从license_detections中获取最佳匹配的详细信息。367 从license_detections中获取最佳匹配的详细信息。
369- 368+ 
370 用于判断匹配质量,决定是否需要从源文件读取完整license文本。369 用于判断匹配质量,决定是否需要从源文件读取完整license文本。
371- 370+ 
372 Args:371 Args:
373 file_info: scancode文件信息372 file_info: scancode文件信息
374- 373+ 
375 Returns:374 Returns:
376 Dict: 包含最佳匹配信息的字典:375 Dict: 包含最佳匹配信息的字典:
377 - has_license_rule: 是否有.LICENSE规则匹配376 - has_license_rule: 是否有.LICENSE规则匹配
@@ -389,17 +388,17 @@ def get_best_match_info(file_info: dict) -> Dict[str, Any]:
389 "end_line": 0,388 "end_line": 0,
390 "rule_identifier": "",389 "rule_identifier": "",
391 }390 }
392- 391+ 
393 best_length = 0392 best_length = 0
394 best_match = None393 best_match = None
395 has_license_rule = False394 has_license_rule = False
396- 395+ 
397 for detection in license_detections:396 for detection in license_detections:
398 matches = detection.get("matches", [])397 matches = detection.get("matches", [])
399 for match in matches:398 for match in matches:
400 rule_id = match.get("rule_identifier", "")399 rule_id = match.get("rule_identifier", "")
401 matched_length = match.get("matched_length", 0)400 matched_length = match.get("matched_length", 0)
402- 401+ 
403 # 优先选择 .LICENSE 规则402 # 优先选择 .LICENSE 规则
404 if rule_id.endswith(".LICENSE"):403 if rule_id.endswith(".LICENSE"):
405 if not has_license_rule or matched_length > best_length:404 if not has_license_rule or matched_length > best_length:
@@ -411,7 +410,7 @@ def get_best_match_info(file_info: dict) -> Dict[str, Any]:
411 if matched_length > best_length:410 if matched_length > best_length:
412 best_match = match411 best_match = match
413 best_length = matched_length412 best_length = matched_length
414- 413+ 
415 if best_match:414 if best_match:
416 return {415 return {
417 "has_license_rule": has_license_rule,416 "has_license_rule": has_license_rule,
@@ -420,7 +419,7 @@ def get_best_match_info(file_info: dict) -> Dict[str, Any]:
420 "end_line": best_match.get("end_line", 0),419 "end_line": best_match.get("end_line", 0),
421 "rule_identifier": best_match.get("rule_identifier", ""),420 "rule_identifier": best_match.get("rule_identifier", ""),
422 }421 }
423- 422+ 
424 return {423 return {
425 "has_license_rule": False,424 "has_license_rule": False,
426 "matched_length": 0,425 "matched_length": 0,
@@ -433,15 +432,15 @@ def get_best_match_info(file_info: dict) -> Dict[str, Any]:
433def _is_valid_license_match(file_info: dict) -> bool:432def _is_valid_license_match(file_info: dict) -> bool:
434 """433 """
435 判断ScanCode对文件的license匹配是否有效(非误匹配)。434 判断ScanCode对文件的license匹配是否有效(非误匹配)。
436- 435+ 
437 过滤规则:436 过滤规则:
438 - 有 .LICENSE 规则匹配 → 有效(完整的license文本匹配)437 - 有 .LICENSE 规则匹配 → 有效(完整的license文本匹配)
439 - 无 .LICENSE 规则但匹配长度 >= 20 → 有效(较长的片段匹配)438 - 无 .LICENSE 规则但匹配长度 >= 20 → 有效(较长的片段匹配)
440 - 无 .LICENSE 规则且匹配长度 < 20 → 无效(短文本引用,如说明文档中提到 "MIT"439 - 无 .LICENSE 规则且匹配长度 < 20 → 无效(短文本引用,如说明文档中提到 "MIT"
441- 440+ 
442 Args:441 Args:
443 file_info: scancode文件信息442 file_info: scancode文件信息
444- 443+ 
445 Returns:444 Returns:
446 bool: 是否是有效的license匹配445 bool: 是否是有效的license匹配
447 """446 """
@@ -460,55 +459,55 @@ def _find_license_by_name(
460) -> List[Dict[str, Any]]:459) -> List[Dict[str, Any]]:
461 """460 """
462 根据license名称查找对应的license记录。461 根据license名称查找对应的license记录。
463- 462+ 
464 仅接受根目录下或LICENSES目录下的license文件。463 仅接受根目录下或LICENSES目录下的license文件。
465- 464+ 
466 当有多个相同license的文件时,按路径优先级筛选:465 当有多个相同license的文件时,按路径优先级筛选:
467 - 根目录下的license文件优先 (如 LICENSE.MIT)466 - 根目录下的license文件优先 (如 LICENSE.MIT)
468 - LICENSES目录下次之 (如 LICENSES/MIT.txt)467 - LICENSES目录下次之 (如 LICENSES/MIT.txt)
469 - 其他目录最低468 - 其他目录最低
470- 469+ 
471 Args:470 Args:
472 data: scancode JSON数据471 data: scancode JSON数据
473 target_license_files: 目标license文件集合472 target_license_files: 目标license文件集合
474 target_license_filenames: 目标license文件名集合473 target_license_filenames: 目标license文件名集合
475 license_name: 要查找的license名称474 license_name: 要查找的license名称
476- 475+ 
477 Returns:476 Returns:
478 List[Dict]: 匹配的license记录列表477 List[Dict]: 匹配的license记录列表
479 """478 """
480 license_name_lower = license_name.lower()479 license_name_lower = license_name.lower()
481 matching_records = []480 matching_records = []
482 seen = set()481 seen = set()
483- 482+ 
484 for file_info in data.get("files", []):483 for file_info in data.get("files", []):
485 file_path = file_info.get("path", "")484 file_path = file_info.get("path", "")
486- 485+ 
487 # 使用严格的路径检查486 # 使用严格的路径检查
488 is_target = _is_valid_license_path(file_path, target_license_files)487 is_target = _is_valid_license_path(file_path, target_license_files)
489- 488+ 
490 if not is_target:489 if not is_target:
491 continue490 continue
492- 491+ 
493 detected_spdx = file_info.get("detected_license_expression_spdx", "")492 detected_spdx = file_info.get("detected_license_expression_spdx", "")
494 detected_license = file_info.get("detected_license_expression", "")493 detected_license = file_info.get("detected_license_expression", "")
495- 494+ 
496 if not detected_spdx:495 if not detected_spdx:
497 continue496 continue
498- 497+ 
499 # 检查是否匹配用户提供的license名称498 # 检查是否匹配用户提供的license名称
500 # 支持部分匹配和忽略大小写499 # 支持部分匹配和忽略大小写
501 spdx_lower = detected_spdx.lower()500 spdx_lower = detected_spdx.lower()
502 license_lower = detected_license.lower()501 license_lower = detected_license.lower()
503- 502+ 
504- if (license_name_lower in spdx_lower or 503+ if (license_name_lower in spdx_lower or
505 license_name_lower in license_lower or504 license_name_lower in license_lower or
506 spdx_lower in license_name_lower or505 spdx_lower in license_name_lower or
507 license_lower in license_name_lower):506 license_lower in license_name_lower):
508- 507+ 
509 # 获取最佳matched_text(优先.LICENSE规则)508 # 获取最佳matched_text(优先.LICENSE规则)
510 matched_text = _get_best_matched_text(file_info)509 matched_text = _get_best_matched_text(file_info)
511- 510+ 
512 # 判断是否有 .LICENSE 规则匹配来自其他文件(跨文件引用)511 # 判断是否有 .LICENSE 规则匹配来自其他文件(跨文件引用)
513 from_file_self = True512 from_file_self = True
514 for det in file_info.get("license_detections", []):513 for det in file_info.get("license_detections", []):
@@ -519,7 +518,7 @@ def _find_license_by_name(
519 break518 break
520 if not from_file_self:519 if not from_file_self:
521 break520 break
522- 521+ 
523 unique_key = (detected_spdx, file_path)522 unique_key = (detected_spdx, file_path)
524 if unique_key not in seen:523 if unique_key not in seen:
525 seen.add(unique_key)524 seen.add(unique_key)
@@ -530,7 +529,7 @@ def _find_license_by_name(
530 "matched_text": matched_text,529 "matched_text": matched_text,
531 "from_file_self": from_file_self,530 "from_file_self": from_file_self,
532 })531 })
533- 532+ 
534 # 按路径优先级去重:当有多个相同SPDX标识的license时,只保留优先级最高的533 # 按路径优先级去重:当有多个相同SPDX标识的license时,只保留优先级最高的
535 # 优先级:根目录(1) > LICENSES目录(2) > 其他目录(3)534 # 优先级:根目录(1) > LICENSES目录(2) > 其他目录(3)
536 if len(matching_records) > 1:535 if len(matching_records) > 1:
@@ -541,7 +540,7 @@ def _find_license_by_name(
541 if spdx not in spdx_groups:540 if spdx not in spdx_groups:
542 spdx_groups[spdx] = []541 spdx_groups[spdx] = []
543 spdx_groups[spdx].append(record)542 spdx_groups[spdx].append(record)
544- 543+ 
545 # 对每组,只保留优先级最高的记录544 # 对每组,只保留优先级最高的记录
546 filtered_records = []545 filtered_records = []
547 for spdx, records in spdx_groups.items():546 for spdx, records in spdx_groups.items():
@@ -552,7 +551,7 @@ def _find_license_by_name(
552 # 1. 路径优先级(根目录优先)551 # 1. 路径优先级(根目录优先)
553 # 2. from_file_self 优先(自身包含 license 全文 > 引用其他文件)552 # 2. from_file_self 优先(自身包含 license 全文 > 引用其他文件)
554 sorted_records = sorted(553 sorted_records = sorted(
555- records, 554+ records,
556 key=lambda r: (555 key=lambda r: (
557 _get_license_path_priority(r["file"]),556 _get_license_path_priority(r["file"]),
558 0 if r.get("from_file_self", True) else 1,557 0 if r.get("from_file_self", True) else 1,
@@ -561,9 +560,9 @@ def _find_license_by_name(
561 # 只取优先级最高的(第一个)560 # 只取优先级最高的(第一个)
562 filtered_records.append(sorted_records[0])561 filtered_records.append(sorted_records[0])
563 debug(f"Multiple {spdx} license files found, selected: {sorted_records[0]['file']}")562 debug(f"Multiple {spdx} license files found, selected: {sorted_records[0]['file']}")
564- 563+ 
565 matching_records = filtered_records564 matching_records = filtered_records
566- 565+ 
567 return matching_records566 return matching_records
568 567 
569 568 
@@ -574,111 +573,111 @@ def _find_license_by_path(
574) -> Optional[Dict[str, Any]]:573) -> Optional[Dict[str, Any]]:
575 """574 """
576 根据license文件路径查找对应的license名称。575 根据license文件路径查找对应的license名称。
577- 576+ 
578 支持处理嵌套目录结构:用户传入 LICENSE.MIT,可以匹配到 json-develop/LICENSE.MIT577 支持处理嵌套目录结构:用户传入 LICENSE.MIT,可以匹配到 json-develop/LICENSE.MIT
579- 578+ 
580 匹配规则(按优先级):579 匹配规则(按优先级):
581 1. 精确匹配:用户路径与JSON路径完全一致580 1. 精确匹配:用户路径与JSON路径完全一致
582- 2. 结尾匹配:JSON路径以 "/用户路径" 结尾(处理嵌套目录) 581+ 2. 结尾匹配:JSON路径以 "/用户路径" 结尾(处理嵌套目录)
583- 582+ 
584 误匹配过滤:583 误匹配过滤:
585 - 只有 .LICENSE 规则匹配才认为是有效的完整license匹配584 - 只有 .LICENSE 规则匹配才认为是有效的完整license匹配
586 - 短文本规则匹配(如 mit_30.RULE 仅匹配2字符)会被过滤585 - 短文本规则匹配(如 mit_30.RULE 仅匹配2字符)会被过滤
587- 586+ 
588 注意:不做纯文件名匹配,避免误匹配其他目录下的同名文件587 注意:不做纯文件名匹配,避免误匹配其他目录下的同名文件
589- 588+ 
590 Args:589 Args:
591 data: scancode JSON数据590 data: scancode JSON数据
592 target_license_files: 目标license文件集合591 target_license_files: 目标license文件集合
593 license_path: 要查找的license文件相对路径592 license_path: 要查找的license文件相对路径
594- 593+ 
595 Returns:594 Returns:
596 Optional[Dict]: 匹配的license记录,如果未找到或为误匹配则返回None595 Optional[Dict]: 匹配的license记录,如果未找到或为误匹配则返回None
597 """596 """
598 # 标准化路径格式:移除前导斜杠,统一路径分隔符597 # 标准化路径格式:移除前导斜杠,统一路径分隔符
599 # 避免 /LICENSES/MIT.txt 被解析为绝对路径598 # 避免 /LICENSES/MIT.txt 被解析为绝对路径
600 license_path_normalized = license_path.lstrip("/").replace("\\", "/").lower()599 license_path_normalized = license_path.lstrip("/").replace("\\", "/").lower()
601- 600+ 
602 for file_info in data.get("files", []):601 for file_info in data.get("files", []):
603 file_path = file_info.get("path", "").replace("\\", "/")602 file_path = file_info.get("path", "").replace("\\", "/")
604 file_path_lower = file_path.lower()603 file_path_lower = file_path.lower()
605- 604+ 
606 # 检查路径是否匹配(只做精确匹配和结尾匹配,避免误匹配)605 # 检查路径是否匹配(只做精确匹配和结尾匹配,避免误匹配)
607 is_match = False606 is_match = False
608- 607+ 
609 # 1. 精确匹配608 # 1. 精确匹配
610 if file_path_lower == license_path_normalized:609 if file_path_lower == license_path_normalized:
611 is_match = True610 is_match = True
612 # 2. 结尾匹配(处理嵌套目录:用户传入 LICENSE.MIT,匹配 json-develop/LICENSE.MIT)611 # 2. 结尾匹配(处理嵌套目录:用户传入 LICENSE.MIT,匹配 json-develop/LICENSE.MIT)
613 elif file_path_lower.endswith("/" + license_path_normalized):612 elif file_path_lower.endswith("/" + license_path_normalized):
614 is_match = True613 is_match = True
615- 614+ 
616 if not is_match:615 if not is_match:
617 continue616 continue
618- 617+ 
619 detected_spdx = file_info.get("detected_license_expression_spdx", "")618 detected_spdx = file_info.get("detected_license_expression_spdx", "")
620 detected_license = file_info.get("detected_license_expression", "")619 detected_license = file_info.get("detected_license_expression", "")
621- 620+ 
622 # 过滤误匹配(短文本引用,如说明文档中提到 license 名称)621 # 过滤误匹配(短文本引用,如说明文档中提到 license 名称)
623 if not _is_valid_license_match(file_info):622 if not _is_valid_license_match(file_info):
624 return None623 return None
625- 624+ 
626 # 获取最佳matched_text(优先.LICENSE规则)625 # 获取最佳matched_text(优先.LICENSE规则)
627 matched_text = _get_best_matched_text(file_info)626 matched_text = _get_best_matched_text(file_info)
628- 627+ 
629 return {628 return {
630 "file": file_path,629 "file": file_path,
631 "spdx_identifier": detected_spdx,630 "spdx_identifier": detected_spdx,
632 "license_expression": detected_license,631 "license_expression": detected_license,
633 "matched_text": matched_text,632 "matched_text": matched_text,
634 }633 }
635- 634+ 
636 return None635 return None
637 636 
638 637 
639def _is_valid_license_path(file_path: str, target_license_files: Set[str]) -> bool:638def _is_valid_license_path(file_path: str, target_license_files: Set[str]) -> bool:
640 """639 """
641 检查license文件路径是否有效。640 检查license文件路径是否有效。
642- 641+ 
643 有效条件(满足其一即可):642 有效条件(满足其一即可):
644 1. 文件在target_license_files集合中643 1. 文件在target_license_files集合中
645 2. 项目根目录下的一级LICENSES目录中的文件(如 project-name/LICENSES/xxx)644 2. 项目根目录下的一级LICENSES目录中的文件(如 project-name/LICENSES/xxx)
646 - LICENSES目录下的文件,即使命名不规范,也会被接受645 - LICENSES目录下的文件,即使命名不规范,也会被接受
647 3. 项目根目录下的license文件(如 project-name/LICENSE.MIT, project-name/MIT.txt)646 3. 项目根目录下的license文件(如 project-name/LICENSE.MIT, project-name/MIT.txt)
648- 647+ 
649 路径结构说明:648 路径结构说明:
650 - 路径格式通常为:项目根目录/文件 或 项目根目录/LICENSES/文件649 - 路径格式通常为:项目根目录/文件 或 项目根目录/LICENSES/文件
651 - 例如:json-develop/LICENSE.MIT, json-develop/LICENSES/CC0-1.0.txt650 - 例如:json-develop/LICENSE.MIT, json-develop/LICENSES/CC0-1.0.txt
652 - 不允许:json-develop/subdir/LICENSES/xxx(LICENSES必须是项目根目录下的一级子目录)651 - 不允许:json-develop/subdir/LICENSES/xxx(LICENSES必须是项目根目录下的一级子目录)
653- 652+ 
654 Args:653 Args:
655 file_path: 文件路径654 file_path: 文件路径
656 target_license_files: 目标license文件集合655 target_license_files: 目标license文件集合
657- 656+ 
658 Returns:657 Returns:
659 bool: 是否是有效的license文件路径658 bool: 是否是有效的license文件路径
660 """659 """
661 # 直接匹配target_license_files集合660 # 直接匹配target_license_files集合
662 if file_path in target_license_files:661 if file_path in target_license_files:
663 return True662 return True
664- 663+ 
665 # 清理路径:移除解压根目录前缀(如 xxx-extract/project-name/ -> project-name/)664 # 清理路径:移除解压根目录前缀(如 xxx-extract/project-name/ -> project-name/)
666 cleaned_path = _clean_scancode_path(file_path)665 cleaned_path = _clean_scancode_path(file_path)
667- 666+ 
668 # 解析清理后的路径667 # 解析清理后的路径
669 path_parts = Path(cleaned_path).parts668 path_parts = Path(cleaned_path).parts
670- 669+ 
671 # 过滤掉空的路径部分(如根目录)670 # 过滤掉空的路径部分(如根目录)
672 path_parts = [p for p in path_parts if p]671 path_parts = [p for p in path_parts if p]
673- 672+ 
674 if not path_parts:673 if not path_parts:
675 return False674 return False
676- 675+ 
677 # 路径深度检查:676 # 路径深度检查:
678 # - 深度为1:不可能出现,因为扫描的是项目根目录下的文件677 # - 深度为1:不可能出现,因为扫描的是项目根目录下的文件
679 # - 深度为2:项目根目录下的文件(如 json-develop/LICENSE.MIT)678 # - 深度为2:项目根目录下的文件(如 json-develop/LICENSE.MIT)
680 # - 深度>=3:可能是 LICENSES 目录下的文件(如 json-develop/LICENSES/CC0-1.0.txt)679 # - 深度>=3:可能是 LICENSES 目录下的文件(如 json-develop/LICENSES/CC0-1.0.txt)
681- 680+ 
682 if len(path_parts) == 1:681 if len(path_parts) == 1:
683 # 不应该出现这种情况,因为项目代码都在项目根目录下682 # 不应该出现这种情况,因为项目代码都在项目根目录下
684 return False683 return False
@@ -703,38 +702,38 @@ def _is_valid_license_path(file_path: str, target_license_files: Set[str]) -> bo
703def _get_license_path_priority(file_path: str) -> int:702def _get_license_path_priority(file_path: str) -> int:
704 """703 """
705 获取license文件路径的优先级。704 获取license文件路径的优先级。
706- 705+ 
707 优先级规则:706 优先级规则:
708 - 根目录下的license文件优先级最高 (1)707 - 根目录下的license文件优先级最高 (1)
709 - LICENSES目录下的文件优先级次之 (2)708 - LICENSES目录下的文件优先级次之 (2)
710 - 其他目录的文件优先级最低 (3)709 - 其他目录的文件优先级最低 (3)
711- 710+ 
712 例如:711 例如:
713 - json-develop/LICENSE.MIT -> 1 (根目录)712 - json-develop/LICENSE.MIT -> 1 (根目录)
714 - json-develop/LICENSES/MIT.txt -> 2 (LICENSES目录)713 - json-develop/LICENSES/MIT.txt -> 2 (LICENSES目录)
715 - json-develop/docs/LICENSE -> 3 (其他目录)714 - json-develop/docs/LICENSE -> 3 (其他目录)
716- 715+ 
717 Args:716 Args:
718 file_path: license文件路径717 file_path: license文件路径
719- 718+ 
720 Returns:719 Returns:
721 int: 优先级数字(越小优先级越高)720 int: 优先级数字(越小优先级越高)
722 """721 """
723 # 清理路径722 # 清理路径
724 cleaned_path = _clean_scancode_path(file_path)723 cleaned_path = _clean_scancode_path(file_path)
725- 724+ 
726 # 标准化路径分隔符725 # 标准化路径分隔符
727 path_parts = cleaned_path.replace("\\", "/").lower().split("/")726 path_parts = cleaned_path.replace("\\", "/").lower().split("/")
728- 727+ 
729 # 过滤掉空部分728 # 过滤掉空部分
730 path_parts = [p for p in path_parts if p]729 path_parts = [p for p in path_parts if p]
731- 730+ 
732 if not path_parts:731 if not path_parts:
733 return 3732 return 3
734- 733+ 
735 # 检查路径深度和结构734 # 检查路径深度和结构
736 # 路径格式:项目名/license文件 或 项目名/LICENSES/license文件735 # 路径格式:项目名/license文件 或 项目名/LICENSES/license文件
737- 736+ 
738 if len(path_parts) == 1:737 if len(path_parts) == 1:
739 # 只有一个部分(不太可能出现在我们的场景中)738 # 只有一个部分(不太可能出现在我们的场景中)
740 return 3739 return 3
@@ -755,33 +754,33 @@ def _get_license_path_priority(file_path: str) -> int:
755def _clean_scancode_path(file_path: str) -> str:754def _clean_scancode_path(file_path: str) -> str:
756 """755 """
757 清理scancode输出的路径,移除解压根目录前缀。756 清理scancode输出的路径,移除解压根目录前缀。
758- 757+ 
759 例如:758 例如:
760 - json-develop.zip-extract/json-develop/LICENSE.MIT -> json-develop/LICENSE.MIT759 - json-develop.zip-extract/json-develop/LICENSE.MIT -> json-develop/LICENSE.MIT
761 - json-develop/LICENSE.MIT -> json-develop/LICENSE.MIT(不变)760 - json-develop/LICENSE.MIT -> json-develop/LICENSE.MIT(不变)
762- 761+ 
763 Args:762 Args:
764 file_path: 原始文件路径763 file_path: 原始文件路径
765- 764+ 
766 Returns:765 Returns:
767 str: 清理后的路径766 str: 清理后的路径
768 """767 """
769 # 标准化路径分隔符768 # 标准化路径分隔符
770 path_parts = file_path.replace("\\", "/").split("/")769 path_parts = file_path.replace("\\", "/").split("/")
771- 770+ 
772 # 过滤掉空部分771 # 过滤掉空部分
773 path_parts = [p for p in path_parts if p]772 path_parts = [p for p in path_parts if p]
774- 773+ 
775 if not path_parts:774 if not path_parts:
776 return file_path775 return file_path
777- 776+ 
778 # 查找并跳过 -extract 目录777 # 查找并跳过 -extract 目录
779 start_idx = 0778 start_idx = 0
780 for i, part in enumerate(path_parts):779 for i, part in enumerate(path_parts):
781 if "-extract" in part.lower():780 if "-extract" in part.lower():
782 start_idx = i + 1781 start_idx = i + 1
783 break782 break
784- 783+ 
785 # 返回清理后的路径784 # 返回清理后的路径
786 return "/".join(path_parts[start_idx:])785 return "/".join(path_parts[start_idx:])
787 786 
@@ -793,55 +792,55 @@ def _extract_all_licenses(
793) -> List[Dict[str, Any]]:792) -> List[Dict[str, Any]]:
794 """793 """
795 提取所有license信息。794 提取所有license信息。
796- 795+ 
797 仅接受:796 仅接受:
798 1. 项目根目录下的license文件797 1. 项目根目录下的license文件
799 2. 根目录下LICENSES目录中的文件798 2. 根目录下LICENSES目录中的文件
800- 799+ 
801 当有多个相同license的文件时,按路径优先级筛选:800 当有多个相同license的文件时,按路径优先级筛选:
802 - 根目录下的license文件优先 (如 LICENSE.MIT)801 - 根目录下的license文件优先 (如 LICENSE.MIT)
803 - LICENSES目录下次之 (如 LICENSES/MIT.txt)802 - LICENSES目录下次之 (如 LICENSES/MIT.txt)
804 - 其他目录最低803 - 其他目录最低
805- 804+ 
806 Args:805 Args:
807 data: scancode JSON数据806 data: scancode JSON数据
808 target_license_files: 目标license文件集合807 target_license_files: 目标license文件集合
809 target_license_filenames: 目标license文件名集合808 target_license_filenames: 目标license文件名集合
810- 809+ 
811 Returns:810 Returns:
812 List[Dict]: 提取的license记录列表811 List[Dict]: 提取的license记录列表
813 """812 """
814 license_records = []813 license_records = []
815 seen = set()814 seen = set()
816- 815+ 
817 for file_info in data.get("files", []):816 for file_info in data.get("files", []):
818 file_path = file_info.get("path", "")817 file_path = file_info.get("path", "")
819- 818+ 
820 # 使用严格的路径检查819 # 使用严格的路径检查
821 is_target = _is_valid_license_path(file_path, target_license_files)820 is_target = _is_valid_license_path(file_path, target_license_files)
822- 821+ 
823 if not is_target:822 if not is_target:
824 continue823 continue
825- 824+ 
826 detected_license = file_info.get("detected_license_expression", "")825 detected_license = file_info.get("detected_license_expression", "")
827 detected_spdx = file_info.get("detected_license_expression_spdx", "")826 detected_spdx = file_info.get("detected_license_expression_spdx", "")
828- 827+ 
829 if not detected_license:828 if not detected_license:
830 continue829 continue
831- 830+ 
832 # 过滤误匹配(短文本引用,如说明文档中提到 license 名称)831 # 过滤误匹配(短文本引用,如说明文档中提到 license 名称)
833 if not _is_valid_license_match(file_info):832 if not _is_valid_license_match(file_info):
834 continue833 continue
835- 834+ 
836 # 初步去重:同时考虑spdx、license和文件路径,避免同一文件重复添加835 # 初步去重:同时考虑spdx、license和文件路径,避免同一文件重复添加
837 unique_key = (detected_spdx, detected_license, file_path)836 unique_key = (detected_spdx, detected_license, file_path)
838 if unique_key in seen:837 if unique_key in seen:
839 continue838 continue
840 seen.add(unique_key)839 seen.add(unique_key)
841- 840+ 
842 # 获取最佳matched_text(优先.LICENSE规则)841 # 获取最佳matched_text(优先.LICENSE规则)
843 matched_text = _get_best_matched_text(file_info)842 matched_text = _get_best_matched_text(file_info)
844- 843+ 
845 # 判断是否有 .LICENSE 规则匹配来自其他文件(跨文件引用)844 # 判断是否有 .LICENSE 规则匹配来自其他文件(跨文件引用)
846 # from_file_self=True 表示文件自身包含 license,False 表示 license 来自被引用的其他文件845 # from_file_self=True 表示文件自身包含 license,False 表示 license 来自被引用的其他文件
847 from_file_self = True846 from_file_self = True
@@ -853,7 +852,7 @@ def _extract_all_licenses(
853 break852 break
854 if not from_file_self:853 if not from_file_self:
855 break854 break
856- 855+ 
857 license_records.append({856 license_records.append({
858 "file": file_path,857 "file": file_path,
859 "spdx_identifier": detected_spdx,858 "spdx_identifier": detected_spdx,
@@ -861,7 +860,7 @@ def _extract_all_licenses(
861 "matched_text": matched_text,860 "matched_text": matched_text,
862 "from_file_self": from_file_self,861 "from_file_self": from_file_self,
863 })862 })
864- 863+ 
865 # 按路径优先级去重:当有多个相同(spdx, license)组合的license时,只保留优先级最高的864 # 按路径优先级去重:当有多个相同(spdx, license)组合的license时,只保留优先级最高的
866 # 优先级:根目录(1) > LICENSES目录(2) > 其他目录(3)865 # 优先级:根目录(1) > LICENSES目录(2) > 其他目录(3)
867 if len(license_records) > 1:866 if len(license_records) > 1:
@@ -872,7 +871,7 @@ def _extract_all_licenses(
872 if key not in spdx_license_groups:871 if key not in spdx_license_groups:
873 spdx_license_groups[key] = []872 spdx_license_groups[key] = []
874 spdx_license_groups[key].append(record)873 spdx_license_groups[key].append(record)
875- 874+ 
876 # 对每组,只保留优先级最高的记录875 # 对每组,只保留优先级最高的记录
877 filtered_records = []876 filtered_records = []
878 for key, records in spdx_license_groups.items():877 for key, records in spdx_license_groups.items():
@@ -883,7 +882,7 @@ def _extract_all_licenses(
883 # 1. 路径优先级(根目录优先)882 # 1. 路径优先级(根目录优先)
884 # 2. from_file_self 优先(自身包含 license 全文 > 引用其他文件)883 # 2. from_file_self 优先(自身包含 license 全文 > 引用其他文件)
885 sorted_records = sorted(884 sorted_records = sorted(
886- records, 885+ records,
887 key=lambda r: (886 key=lambda r: (
888 _get_license_path_priority(r["file"]),887 _get_license_path_priority(r["file"]),
889 0 if r.get("from_file_self", True) else 1,888 0 if r.get("from_file_self", True) else 1,
@@ -893,9 +892,9 @@ def _extract_all_licenses(
893 filtered_records.append(sorted_records[0])892 filtered_records.append(sorted_records[0])
894 spdx, license = key893 spdx, license = key
895 debug(f"Multiple ({spdx}, {license}) license files found, selected: {sorted_records[0]['file']}")894 debug(f"Multiple ({spdx}, {license}) license files found, selected: {sorted_records[0]['file']}")
896- 895+ 
897 license_records = filtered_records896 license_records = filtered_records
898- 897+ 
899 info(f"Extracted {len(license_records)} unique license records")898 info(f"Extracted {len(license_records)} unique license records")
900 return license_records899 return license_records
901 900 
@@ -908,7 +907,7 @@ def write_license_report(
908) -> None:907) -> None:
909 """908 """
910 写入license报告。909 写入license报告。
911- 910+ 
912 Args:911 Args:
913 output_txt: 输出文件路径912 output_txt: 输出文件路径
914 software_name: 软件名称913 software_name: 软件名称
@@ -919,16 +918,16 @@ def write_license_report(
919 f.write("=" * 60 + "\n")918 f.write("=" * 60 + "\n")
920 f.write("LICENSE INFORMATION EXTRACTION REPORT\n")919 f.write("LICENSE INFORMATION EXTRACTION REPORT\n")
921 f.write("=" * 60 + "\n\n")920 f.write("=" * 60 + "\n\n")
922- 921+ 
923 # 软件基本信息922 # 软件基本信息
924 f.write(f"Software Name: {software_name}\n")923 f.write(f"Software Name: {software_name}\n")
925 f.write(f"Software Version: {software_version}\n\n")924 f.write(f"Software Version: {software_version}\n\n")
926- 925+ 
927 if not license_records:926 if not license_records:
928 f.write("No license information found.\n")927 f.write("No license information found.\n")
929 else:928 else:
930 f.write(f"Total unique licenses found: {len(license_records)}\n\n")929 f.write(f"Total unique licenses found: {len(license_records)}\n\n")
931- 930+ 
932 for i, record in enumerate(license_records, 1):931 for i, record in enumerate(license_records, 1):
933 f.write(f"--- License #{i} ---\n")932 f.write(f"--- License #{i} ---\n")
934 f.write(f"File: {record.get('file', 'N/A')}\n")933 f.write(f"File: {record.get('file', 'N/A')}\n")
@@ -941,5 +940,5 @@ def write_license_report(
941 text = text[:500] + "..."940 text = text[:500] + "..."
942 f.write(f"Matched Text:\n{text}\n")941 f.write(f"Matched Text:\n{text}\n")
943 f.write("\n")942 f.write("\n")
944- 943+ 
945- info(f"License report written to {output_txt}")944+ info(f"License report written to {output_txt}")
@@ -79,25 +79,25 @@ or directories and generates a standardized Readme.opensource file.
79 79 
80场景1: 完全自动提取80场景1: 完全自动提取
81 cret -t package.zip -n "MyLib" -v "1.0.0"81 cret -t package.zip -n "MyLib" -v "1.0.0"
82- 82+ 
83 说明: 工具自动扫描并提取所有版权和许可证信息83 说明: 工具自动扫描并提取所有版权和许可证信息
84 Note: Tool automatically scans and extracts all copyright and license info84 Note: Tool automatically scans and extracts all copyright and license info
85 85 
86场景2: 指定许可证名称86场景2: 指定许可证名称
87 cret -t package.zip -n "MyLib" -v "1.0.0" -l "MIT"87 cret -t package.zip -n "MyLib" -v "1.0.0" -l "MIT"
88- 88+ 
89 说明: 当已知许可证名称时,可指定以获得更准确的结果89 说明: 当已知许可证名称时,可指定以获得更准确的结果
90 Note: Specify license name when known for more accurate results90 Note: Specify license name when known for more accurate results
91 91 
92场景3: 指定许可证文件路径92场景3: 指定许可证文件路径
93 cret -t package.zip -n "MyLib" -v "1.0.0" -p "LICENSE"93 cret -t package.zip -n "MyLib" -v "1.0.0" -p "LICENSE"
94- 94+ 
95 说明: 当已知许可证文件位置时,可指定路径95 说明: 当已知许可证文件位置时,可指定路径
96 Note: Specify license file path when known96 Note: Specify license file path when known
97 97 
98场景4: 同时指定许可证名称和路径 (推荐 / Recommended)98场景4: 同时指定许可证名称和路径 (推荐 / Recommended)
99 cret -t package.zip -n "MyLib" -v "1.0.0" -l "MIT" -p "LICENSE"99 cret -t package.zip -n "MyLib" -v "1.0.0" -l "MIT" -p "LICENSE"
100- 100+ 
101 说明: 同时提供名称和路径,跳过自动检测,结果最准确101 说明: 同时提供名称和路径,跳过自动检测,结果最准确
102 Note: Provide both to skip automatic detection, most accurate results102 Note: Provide both to skip automatic detection, most accurate results
103 103 
@@ -187,7 +187,7 @@ Run 'cret --guide' for detailed usage instructions.
187运行 'cret --guide' 查看详细使用指南。187运行 'cret --guide' 查看详细使用指南。
188 """188 """
189 )189 )
190- 190+ 
191 # 必填参数191 # 必填参数
192 parser.add_argument(192 parser.add_argument(
193 "-t", "--target",193 "-t", "--target",
@@ -204,7 +204,7 @@ Run 'cret --guide' for detailed usage instructions.
204 required=True,204 required=True,
205 help="Software version / 软件版本号 (required)"205 help="Software version / 软件版本号 (required)"
206 )206 )
207- 207+ 
208 # 可选参数208 # 可选参数
209 parser.add_argument(209 parser.add_argument(
210 "-l", "--license",210 "-l", "--license",
@@ -241,42 +241,42 @@ Run 'cret --guide' for detailed usage instructions.
241 action="store_true",241 action="store_true",
242 help="Show detailed usage guide / 显示详细使用指南"242 help="Show detailed usage guide / 显示详细使用指南"
243 )243 )
244- 244+ 
245 # 先检查是否只是要显示指南(在解析必填参数之前)245 # 先检查是否只是要显示指南(在解析必填参数之前)
246- if "--guide" in sys.argv or "-g" in sys.argv:246+ if "--guide" in sys.argv:
247 print_guide()247 print_guide()
248 sys.exit(0)248 sys.exit(0)
249- 249+ 
250 args = parser.parse_args()250 args = parser.parse_args()
251- 251+ 
252 return args252 return args
253 253 
254 254 
255def main():255def main():
256 args = parse_args()256 args = parse_args()
257- 257+ 
258 # 解析目标路径258 # 解析目标路径
259 target = Path(args.target).resolve()259 target = Path(args.target).resolve()
260 if not target.exists():260 if not target.exists():
261 error_exit(f"Target does not exist: {target}")261 error_exit(f"Target does not exist: {target}")
262- 262+ 
263 # 确定输出目录263 # 确定输出目录
264 if args.output_dir:264 if args.output_dir:
265 output_dir = Path(args.output_dir).resolve()265 output_dir = Path(args.output_dir).resolve()
266 output_dir.mkdir(parents=True, exist_ok=True)266 output_dir.mkdir(parents=True, exist_ok=True)
267 else:267 else:
268 output_dir = target.parent268 output_dir = target.parent
269- 269+ 
270 # 输出文件路径270 # 输出文件路径
271 result_json = output_dir / "result.json"271 result_json = output_dir / "result.json"
272 output_copyright = output_dir / f"{target.stem}_copyright"272 output_copyright = output_dir / f"{target.stem}_copyright"
273 output_license = output_dir / f"{target.stem}_license"273 output_license = output_dir / f"{target.stem}_license"
274 output_readme = output_dir / "Readme.opensource"274 output_readme = output_dir / "Readme.opensource"
275- 275+ 
276 # 用于清理的变量276 # 用于清理的变量
277 extract_dir = None277 extract_dir = None
278 scan_target = target278 scan_target = target
279- 279+ 
280 # 定义清理函数280 # 定义清理函数
281 def cleanup():281 def cleanup():
282 """清理过程文件和临时目录"""282 """清理过程文件和临时目录"""
@@ -293,24 +293,24 @@ def main():
293 nonlocal extract_dir293 nonlocal extract_dir
294 if extract_dir:294 if extract_dir:
295 cleanup_extract_dir(extract_dir)295 cleanup_extract_dir(extract_dir)
296- 296+ 
297 # 注册清理回调297 # 注册清理回调
298 register_cleanup_callback(cleanup)298 register_cleanup_callback(cleanup)
299- 299+ 
300 # 软件信息300 # 软件信息
301 software_name = args.name301 software_name = args.name
302 software_version = args.version302 software_version = args.version
303 license_name = args.license_name303 license_name = args.license_name
304 license_path = args.license_path304 license_path = args.license_path
305- 305+ 
306 # 设置软件信息(用于错误日志输出)306 # 设置软件信息(用于错误日志输出)
307 set_software_info(software_name, software_version)307 set_software_info(software_name, software_version)
308- 308+ 
309 info(f"Software Name: {software_name}")309 info(f"Software Name: {software_name}")
310 info(f"Software Version: {software_version}")310 info(f"Software Version: {software_version}")
311 info(f"License Name: {license_name or 'Not provided'}")311 info(f"License Name: {license_name or 'Not provided'}")
312 info(f"License Path: {license_path or 'Not provided'}")312 info(f"License Path: {license_path or 'Not provided'}")
313- 313+ 
314 try:314 try:
315 if target.is_file():315 if target.is_file():
316 extract_dir = run_extractcode(target)316 extract_dir = run_extractcode(target)
@@ -330,7 +330,7 @@ def main():
330 info(f"Detected nested directory, using as root: {scan_target}")330 info(f"Detected nested directory, using as root: {scan_target}")
331 except Exception:331 except Exception:
332 pass332 pass
333- 333+ 
334 # 验证用户提供的license路径是否存在334 # 验证用户提供的license路径是否存在
335 if license_path:335 if license_path:
336 # 标准化路径:移除前导斜杠,统一路径分隔符336 # 标准化路径:移除前导斜杠,统一路径分隔符
@@ -339,18 +339,18 @@ def main():
339 license_full_path = scan_target / normalized_license_path339 license_full_path = scan_target / normalized_license_path
340 if not license_full_path.exists():340 if not license_full_path.exists():
341 error_exit(f"License file not found: {license_path}")341 error_exit(f"License file not found: {license_path}")
342- 342+ 
343 # 标准化后的路径用于后续处理343 # 标准化后的路径用于后续处理
344 license_path = normalized_license_path344 license_path = normalized_license_path
345- 345+ 
346 # 执行scancode扫描(始终扫描license,用于校验用户提供的参数)346 # 执行scancode扫描(始终扫描license,用于校验用户提供的参数)
347 check_scancode_available()347 check_scancode_available()
348 scan_data = run_scancode(scan_target, result_json, scan_license=True,348 scan_data = run_scancode(scan_target, result_json, scan_license=True,
349 jobs=args.jobs, max_in_memory=args.max_in_memory)349 jobs=args.jobs, max_in_memory=args.max_in_memory)
350- 350+ 
351 # 提取copyright信息351 # 提取copyright信息
352 copyright_records = extract_and_duplicate_copyright(scan_data, output_copyright)352 copyright_records = extract_and_duplicate_copyright(scan_data, output_copyright)
353- 353+ 
354 # 处理license信息(始终调用process_license_params以执行校验)354 # 处理license信息(始终调用process_license_params以执行校验)
355 license_records = process_license_params(355 license_records = process_license_params(
356 data=scan_data,356 data=scan_data,
@@ -358,11 +358,11 @@ def main():
358 license_name=license_name,358 license_name=license_name,
359 license_path=license_path,359 license_path=license_path,
360 )360 )
361- 361+ 
362 # 检查是否有错误发生(用于非error_exit方式的错误)362 # 检查是否有错误发生(用于非error_exit方式的错误)
363 if has_error():363 if has_error():
364 error_exit("Errors occurred during processing, aborting.")364 error_exit("Errors occurred during processing, aborting.")
365- 365+ 
366 # 写入license报告366 # 写入license报告
367 write_license_report(367 write_license_report(
368 output_txt=output_license,368 output_txt=output_license,
@@ -370,7 +370,7 @@ def main():
370 software_version=software_version,370 software_version=software_version,
371 license_records=license_records,371 license_records=license_records,
372 )372 )
373- 373+ 
374 # 生成Readme.opensource文件374 # 生成Readme.opensource文件
375 write_readme_opensource(375 write_readme_opensource(
376 output_path=output_readme,376 output_path=output_readme,
@@ -394,4 +394,4 @@ def main():
394 debug(f"Result JSON kept at: {result_json}")394 debug(f"Result JSON kept at: {result_json}")
395 395 
396if __name__ == "__main__":396if __name__ == "__main__":
397- main()397+ main()
@@ -12,7 +12,6 @@
12# See the License for the specific language governing permissions and12# See the License for the specific language governing permissions and
13# limitations under the License.13# limitations under the License.
14 14 
15-import json
16from pathlib import Path15from pathlib import Path
17 16 
18from .logger import info17from .logger import info
@@ -24,11 +23,11 @@ def extract_and_duplicate_copyright(
24) -> list:23) -> list:
25 """24 """
26 从scancode结果中提取copyright信息并写入文件。25 从scancode结果中提取copyright信息并写入文件。
27- 26+ 
28 Args:27 Args:
29 data: scancode输出的JSON数据(已解析的字典)28 data: scancode输出的JSON数据(已解析的字典)
30 output_txt: 输出文件路径29 output_txt: 输出文件路径
31- 30+ 
32 Returns:31 Returns:
33 list: 提取的copyright记录列表32 list: 提取的copyright记录列表
34 """33 """
@@ -50,7 +49,7 @@ def extract_and_duplicate_copyright(
50 )49 )
51 if not value:50 if not value:
52 continue51 continue
53- 52+ 
54 value_stripped = value.strip()53 value_stripped = value.strip()
55 54 
56 if "copyright" not in value_stripped.lower():55 if "copyright" not in value_stripped.lower():
@@ -66,6 +65,5 @@ def extract_and_duplicate_copyright(
66 65 
67 info(f"Extracted {len(duplicated_results)} unique records")66 info(f"Extracted {len(duplicated_results)} unique records")
68 info(f"Output written to {output_txt}")67 info(f"Output written to {output_txt}")
69-
70- return duplicated_results
71 68 
69+ return duplicated_results
@@ -14,16 +14,14 @@
14 14 
15import subprocess15import subprocess
16 16 
17+from .config import EXPECTED_SCANCODE_VERSION
17from .logger import ok, warn18from .logger import ok, warn
18 19 
19-# 期望的 scancode-toolkit 版本
20-EXPECTED_SCANCODE_VERSION = "32.5.0"
21- 
22 20 
23def check_scancode_available():21def check_scancode_available():
24 """22 """
25 Check whether scancode command is available and check version.23 Check whether scancode command is available and check version.
26- 24+ 
27 检查 scancode 命令是否可用,如果版本不匹配则告警但不阻止运行。25 检查 scancode 命令是否可用,如果版本不匹配则告警但不阻止运行。
28 """26 """
29 try:27 try:
@@ -41,7 +39,7 @@ def check_scancode_available():
41 )39 )
42 40 
43 version_output = result.stdout.strip()41 version_output = result.stdout.strip()
44- 42+ 
45 # 检查版本是否匹配,不匹配告警但不阻止运行43 # 检查版本是否匹配,不匹配告警但不阻止运行
46 if EXPECTED_SCANCODE_VERSION not in version_output:44 if EXPECTED_SCANCODE_VERSION not in version_output:
47 warn(45 warn(
@@ -49,4 +47,4 @@ def check_scancode_available():
49 f"got {version_output}. This may cause unexpected behavior."47 f"got {version_output}. This may cause unexpected behavior."
50 )48 )
51 else:49 else:
52- ok(f"Scancode available: {version_output}")50+ ok(f"Scancode available: {version_output}")
@@ -45,7 +45,7 @@ def write_readme_opensource(
45) -> None:45) -> None:
46 """46 """
47 生成Readme.opensource文件。47 生成Readme.opensource文件。
48- 48+ 
49 文件格式:49 文件格式:
50 - Software: 软件名称 版本号50 - Software: 软件名称 版本号
51 - Copyright Notice(s):51 - Copyright Notice(s):
@@ -54,7 +54,7 @@ def write_readme_opensource(
54 - Full License Text:54 - Full License Text:
55 - [license文件内容]55 - [license文件内容]
56 - (多个license重复上述两步)56 - (多个license重复上述两步)
57- 57+ 
58 Args:58 Args:
59 output_path: 输出文件路径59 output_path: 输出文件路径
60 software_name: 软件名称60 software_name: 软件名称
@@ -70,10 +70,10 @@ def write_readme_opensource(
70 with output_path.open("w", encoding="utf-8") as f:70 with output_path.open("w", encoding="utf-8") as f:
71 # 第一步:输出软件名称和版本71 # 第一步:输出软件名称和版本
72 f.write(f"Software: {software_name} {software_version}\n")72 f.write(f"Software: {software_name} {software_version}\n")
73- 73+ 
74 # 第二步:输出Copyright Notice(s)74 # 第二步:输出Copyright Notice(s)
75 f.write("Copyright Notice(s):\n")75 f.write("Copyright Notice(s):\n")
76- 76+ 
77 if copyright_records:77 if copyright_records:
78 for copyright_text in copyright_records:78 for copyright_text in copyright_records:
79 # 清理每行并写入79 # 清理每行并写入
@@ -82,31 +82,31 @@ def write_readme_opensource(
82 f.write(f"{cleaned_text}\n")82 f.write(f"{cleaned_text}\n")
83 else:83 else:
84 f.write("No copyright information found.\n")84 f.write("No copyright information found.\n")
85- 85+ 
86 # 第三步:输出License信息(每个license重复)86 # 第三步:输出License信息(每个license重复)
87 if license_records:87 if license_records:
88 for record in license_records:88 for record in license_records:
89 license_name = record.get("spdx_identifier", "Unknown")89 license_name = record.get("spdx_identifier", "Unknown")
90 license_file_path = record.get("file", "")90 license_file_path = record.get("file", "")
91- 91+ 
92 # 输出License名称92 # 输出License名称
93 f.write(f"License: {license_name}\n")93 f.write(f"License: {license_name}\n")
94- 94+ 
95 # 输出Full License Text95 # 输出Full License Text
96 f.write("Full License Text:\n")96 f.write("Full License Text:\n")
97- 97+ 
98 # 尝试读取license文件内容98 # 尝试读取license文件内容
99 license_content_found = False99 license_content_found = False
100- 100+ 
101 if license_file_path and root_path:101 if license_file_path and root_path:
102 # 清理路径:去掉可能存在的扫描前缀目录102 # 清理路径:去掉可能存在的扫描前缀目录
103 # scancode路径可能类似: json-develop.zip-extract/json-develop/LICENSES/xxx.txt103 # scancode路径可能类似: json-develop.zip-extract/json-develop/LICENSES/xxx.txt
104 # 我们需要提取实际的相对路径部分104 # 我们需要提取实际的相对路径部分
105 cleaned_license_path = _clean_license_path(license_file_path, root_path)105 cleaned_license_path = _clean_license_path(license_file_path, root_path)
106- 106+ 
107 # 尝试多种路径组合107 # 尝试多种路径组合
108 possible_paths = _get_possible_license_paths(root_path, cleaned_license_path)108 possible_paths = _get_possible_license_paths(root_path, cleaned_license_path)
109- 109+ 
110 for full_license_path in possible_paths:110 for full_license_path in possible_paths:
111 debug(f"Trying license file path: {full_license_path}")111 debug(f"Trying license file path: {full_license_path}")
112 if full_license_path.exists() and full_license_path.is_file():112 if full_license_path.exists() and full_license_path.is_file():
@@ -122,10 +122,9 @@ def write_readme_opensource(
122 break122 break
123 except Exception as e:123 except Exception as e:
124 debug(f"Failed to read license file {full_license_path}: {e}")124 debug(f"Failed to read license file {full_license_path}: {e}")
125- continue
126 else:125 else:
127 debug(f"Path does not exist or is not a file: {full_license_path}")126 debug(f"Path does not exist or is not a file: {full_license_path}")
128- 127+ 
129 # 如果文件读取失败,尝试使用matched_text128 # 如果文件读取失败,尝试使用matched_text
130 if not license_content_found:129 if not license_content_found:
131 matched_text = record.get("matched_text", "")130 matched_text = record.get("matched_text", "")
@@ -141,44 +140,44 @@ def write_readme_opensource(
141 f.write("License: No license information found.\n")140 f.write("License: No license information found.\n")
142 f.write("Full License Text:\n")141 f.write("Full License Text:\n")
143 f.write("[No license text available]\n")142 f.write("[No license text available]\n")
144- 143+ 
145 info(f"Readme.opensource written to {output_path}")144 info(f"Readme.opensource written to {output_path}")
146 145 
147 146 
148def _clean_license_path(license_file_path: str, root_path: Path) -> str:147def _clean_license_path(license_file_path: str, root_path: Path) -> str:
149 """148 """
150 清理license文件路径,移除扫描前缀目录。149 清理license文件路径,移除扫描前缀目录。
151- 150+ 
152 路径标准化处理:151 路径标准化处理:
153 1. 统一使用正斜杠作为路径分隔符152 1. 统一使用正斜杠作为路径分隔符
154 2. 移除解压根目录前缀153 2. 移除解压根目录前缀
155- 154+ 
156 Args:155 Args:
157 license_file_path: 原始license文件路径156 license_file_path: 原始license文件路径
158 root_path: 根目录路径157 root_path: 根目录路径
159- 158+ 
160 Returns:159 Returns:
161 str: 清理后的相对路径160 str: 清理后的相对路径
162 """161 """
163 # 标准化路径分隔符(统一使用正斜杠)162 # 标准化路径分隔符(统一使用正斜杠)
164 normalized_path = license_file_path.replace("\\", "/")163 normalized_path = license_file_path.replace("\\", "/")
165 path_parts = normalized_path.split("/")164 path_parts = normalized_path.split("/")
166- 165+ 
167 # 过滤空部分166 # 过滤空部分
168 path_parts = [p for p in path_parts if p]167 path_parts = [p for p in path_parts if p]
169- 168+ 
170 if not path_parts:169 if not path_parts:
171 return ""170 return ""
172- 171+ 
173 # 获取root_path的标准化名称(用于匹配)172 # 获取root_path的标准化名称(用于匹配)
174 root_name = root_path.name173 root_name = root_path.name
175- 174+ 
176 # 尝试找到root_path名称在路径中的位置175 # 尝试找到root_path名称在路径中的位置
177 for i, part in enumerate(path_parts):176 for i, part in enumerate(path_parts):
178 if part == root_name:177 if part == root_name:
179 # 返回该位置之后的路径178 # 返回该位置之后的路径
180 return "/".join(path_parts[i+1:])179 return "/".join(path_parts[i+1:])
181- 180+ 
182 # 如果没找到,尝试查找常见的解压目录模式181 # 如果没找到,尝试查找常见的解压目录模式
183 start_idx = 0182 start_idx = 0
184 for i, part in enumerate(path_parts):183 for i, part in enumerate(path_parts):
@@ -186,7 +185,7 @@ def _clean_license_path(license_file_path: str, root_path: Path) -> str:
186 if "-extract" in part.lower():185 if "-extract" in part.lower():
187 start_idx = i + 1186 start_idx = i + 1
188 continue187 continue
189- 188+ 
190 # 返回清理后的路径189 # 返回清理后的路径
191 return "/".join(path_parts[start_idx:])190 return "/".join(path_parts[start_idx:])
192 191 
@@ -194,25 +193,25 @@ def _clean_license_path(license_file_path: str, root_path: Path) -> str:
194def _get_possible_license_paths(root_path: Path, relative_path: str) -> List[Path]:193def _get_possible_license_paths(root_path: Path, relative_path: str) -> List[Path]:
195 """194 """
196 生成可能的license文件完整路径列表。195 生成可能的license文件完整路径列表。
197- 196+ 
198 匹配规则(按优先级):197 匹配规则(按优先级):
199 1. 直接拼接路径198 1. 直接拼接路径
200 2. 处理嵌套目录结构(根目录只有一个子目录的情况)199 2. 处理嵌套目录结构(根目录只有一个子目录的情况)
201- 200+ 
202 注意:不做递归查找,避免误匹配其他目录下的同名文件201 注意:不做递归查找,避免误匹配其他目录下的同名文件
203- 202+ 
204 Args:203 Args:
205 root_path: 根目录路径204 root_path: 根目录路径
206 relative_path: 相对路径205 relative_path: 相对路径
207- 206+ 
208 Returns:207 Returns:
209 List[Path]: 可能的完整路径列表208 List[Path]: 可能的完整路径列表
210 """209 """
211 possible_paths = []210 possible_paths = []
212- 211+ 
213 # 1. 直接拼接212 # 1. 直接拼接
214 possible_paths.append(root_path / relative_path)213 possible_paths.append(root_path / relative_path)
215- 214+ 
216 # 2. 检查是否是嵌套目录结构215 # 2. 检查是否是嵌套目录结构
217 try:216 try:
218 items = list(root_path.iterdir())217 items = list(root_path.iterdir())
@@ -220,8 +219,8 @@ def _get_possible_license_paths(root_path: Path, relative_path: str) -> List[Pat
220 possible_paths.append(items[0] / relative_path)219 possible_paths.append(items[0] / relative_path)
221 except Exception:220 except Exception:
222 pass221 pass
223- 222+ 
224 # 注意:移除了递归查找逻辑,避免误匹配223 # 注意:移除了递归查找逻辑,避免误匹配
225 # 例如:避免将 docs/LICENSE 或 third_party/lib/LICENSE 误匹配为根目录的 LICENSE224 # 例如:避免将 docs/LICENSE 或 third_party/lib/LICENSE 误匹配为根目录的 LICENSE
226- 225+ 
227 return possible_paths226 return possible_paths