已合并
fix: 代码检视修复 #116
IceT_5创建于 8月7日
fix: 代码检视修复 #116
已合并
共 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" | |||
| 67 | cret -t package.zip -n "MySoftware" -v "1.0.0" -o "./output" | 66 | cret -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 30 | 69 | +cret -t package.zip -n "MySoftware" -v "1.0.0" -j 2 --max-in-memory 1000 |
| 71 | 70 | ||
| 72 | # 显示详细使用指南 | 71 | # 显示详细使用指南 |
| 73 | cret --guide | 72 | cret --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.0 | 28 | r"^copying[-_.].*", # COPYING.LESSER, COPYING.GPL, COPYING-3.0 |
| 29 | r"^copyright$", # COPYRIGHT, copyright | 29 | r"^copyright$", # COPYRIGHT, copyright |
| 30 | r"^copyright[-_.].*", # COPYRIGHT.txt, COPYRIGHT.md | 30 | r"^copyright[-_.].*", # COPYRIGHT.txt, COPYRIGHT.md |
| 31 | - | 31 | + |
| 32 | # 按许可证名称匹配(支持SPDX标识符格式) | 32 | # 按许可证名称匹配(支持SPDX标识符格式) |
| 33 | # MIT 系列 | 33 | # MIT 系列 |
| 34 | r"^mit$", # MIT.txt | 34 | r"^mit$", # MIT.txt |
| 35 | r"^mit[-_.].*", # MIT-License, MIT.txt, MIT-0 | 35 | r"^mit[-_.].*", # MIT-License, MIT.txt, MIT-0 |
| 36 | - | 36 | + |
| 37 | # Apache 系列 | 37 | # Apache 系列 |
| 38 | r"^apache$", # Apache.txt | 38 | r"^apache$", # Apache.txt |
| 39 | r"^apache[-_.].*", # Apache-2.0, Apache-License, Apache.txt | 39 | r"^apache[-_.].*", # Apache-2.0, Apache-License, Apache.txt |
| 40 | - | 40 | + |
| 41 | # BSD 系列 | 41 | # BSD 系列 |
| 42 | r"^bsd$", # BSD.txt | 42 | r"^bsd$", # BSD.txt |
| 43 | r"^bsd[-_.].*", # BSD-2-Clause, BSD-3-Clause, BSD-4-Clause, BSD.txt | 43 | r"^bsd[-_.].*", # BSD-2-Clause, BSD-3-Clause, BSD-4-Clause, BSD.txt |
| 44 | - | 44 | + |
| 45 | # GPL 系列 | 45 | # GPL 系列 |
| 46 | r"^gpl$", # GPL.txt | 46 | r"^gpl$", # GPL.txt |
| 47 | r"^gpl[-_.].*", # GPL-3.0, GPL-2.0, GPL-3.0-only, GPL-3.0-or-later | 47 | 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.txt | 50 | r"^lgpl$", # LGPL.txt |
| 51 | r"^lgpl[-_.].*", # LGPL-3.0, LGPL-2.1, LGPL.txt | 51 | r"^lgpl[-_.].*", # LGPL-3.0, LGPL-2.1, LGPL.txt |
| 52 | - | 52 | + |
| 53 | # AGPL 系列 | 53 | # AGPL 系列 |
| 54 | r"^agpl$", # AGPL.txt | 54 | r"^agpl$", # AGPL.txt |
| 55 | r"^agpl[-_.].*", # AGPL-3.0, AGPL-3.0-only | 55 | r"^agpl[-_.].*", # AGPL-3.0, AGPL-3.0-only |
| 56 | - | 56 | + |
| 57 | # MPL 系列 | 57 | # MPL 系列 |
| 58 | r"^mpl$", # MPL.txt | 58 | r"^mpl$", # MPL.txt |
| 59 | r"^mpl[-_.].*", # MPL-2.0, MPL.txt | 59 | r"^mpl[-_.].*", # MPL-2.0, MPL.txt |
| 60 | - | 60 | + |
| 61 | # EPL (Eclipse Public License) 系列 | 61 | # EPL (Eclipse Public License) 系列 |
| 62 | r"^epl$", # EPL.txt | 62 | r"^epl$", # EPL.txt |
| 63 | r"^epl[-_.].*", # EPL-1.0, EPL-2.0, EPL.txt | 63 | r"^epl[-_.].*", # EPL-1.0, EPL-2.0, EPL.txt |
| 64 | - | 64 | + |
| 65 | # Creative Commons 系列 | 65 | # Creative Commons 系列 |
| 66 | r"^cc0$", # CC0.txt | 66 | r"^cc0$", # CC0.txt |
| 67 | r"^cc0[-_.].*", # CC0-1.0, CC0.txt | 67 | r"^cc0[-_.].*", # CC0-1.0, CC0.txt |
| 68 | r"^cc[-_.].*", # CC-BY, CC-BY-SA, CC-BY-4.0 | 68 | r"^cc[-_.].*", # CC-BY, CC-BY-SA, CC-BY-4.0 |
| 69 | - | 69 | + |
| 70 | # ISC License | 70 | # ISC License |
| 71 | r"^isc$", # ISC.txt | 71 | r"^isc$", # ISC.txt |
| 72 | r"^isc[-_.].*", # ISC-License | 72 | r"^isc[-_.].*", # ISC-License |
| 73 | - | 73 | + |
| 74 | # Unlicense | 74 | # Unlicense |
| 75 | r"^unlicense$", # UNLICENSE | 75 | r"^unlicense$", # UNLICENSE |
| 76 | r"^unlicense[-_.].*", # UNLICENSE.txt | 76 | r"^unlicense[-_.].*", # UNLICENSE.txt |
| 77 | - | 77 | + |
| 78 | # zlib License | 78 | # zlib License |
| 79 | r"^zlib$", # zlib.txt | 79 | r"^zlib$", # zlib.txt |
| 80 | r"^zlib[-_.].*", # zlib-license | 80 | r"^zlib[-_.].*", # zlib-license |
| 81 | - | 81 | + |
| 82 | # PostgreSQL License | 82 | # PostgreSQL License |
| 83 | r"^postgresql$", # PostgreSQL.txt | 83 | r"^postgresql$", # PostgreSQL.txt |
| 84 | r"^postgresql[-_.].*", # PostgreSQL-License | 84 | r"^postgresql[-_.].*", # PostgreSQL-License |
| 85 | - | 85 | + |
| 86 | # Open Font License | 86 | # Open Font License |
| 87 | r"^ofl$", # OFL | 87 | r"^ofl$", # OFL |
| 88 | r"^ofl[-_.].*", # OFL.txt, OFL-1.1 | 88 | r"^ofl[-_.].*", # OFL.txt, OFL-1.1 |
| 89 | - | 89 | + |
| 90 | # Artistic License | 90 | # Artistic License |
| 91 | r"^artistic$", # Artistic.txt | 91 | r"^artistic$", # Artistic.txt |
| 92 | r"^artistic[-_.].*", # Artistic-2.0, Artistic-License | 92 | r"^artistic[-_.].*", # Artistic-2.0, Artistic-License |
| 93 | - | 93 | + |
| 94 | # 0BSD (Zero-Clause BSD) | 94 | # 0BSD (Zero-Clause BSD) |
| 95 | r"^0bsd$", # 0BSD.txt | 95 | r"^0bsd$", # 0BSD.txt |
| 96 | - | 96 | + |
| 97 | # Boost Software License | 97 | # Boost Software License |
| 98 | r"^bsl$", # BSL.txt | 98 | r"^bsl$", # BSL.txt |
| 99 | r"^bsl[-_.].*", # BSL-1.0 | 99 | r"^bsl[-_.].*", # BSL-1.0 |
| 100 | r"^boost[-_.].*", # Boost-1.0, Boost-License | 100 | 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.txt | 103 | r"^eupl$", # EUPL.txt |
| 104 | r"^eupl[-_.].*", # EUPL-1.1, EUPL-1.2 | 104 | 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.txt | 107 | r"^cddl$", # CDDL.txt |
| 108 | r"^cddl[-_.].*", # CDDL-1.0, CDDL-1.1 | 108 | r"^cddl[-_.].*", # CDDL-1.0, CDDL-1.1 |
| 109 | - | 109 | + |
| 110 | # Eclipse Distribution License | 110 | # Eclipse Distribution License |
| 111 | r"^edl$", # EDL.txt | 111 | r"^edl$", # EDL.txt |
| 112 | r"^edl[-_.].*", # EDL-1.0 | 112 | r"^edl[-_.].*", # EDL-1.0 |
| 113 | - | 113 | + |
| 114 | # LaTeX Project Public License | 114 | # LaTeX Project Public License |
| 115 | r"^lppl$", # LPPL.txt | 115 | r"^lppl$", # LPPL.txt |
| 116 | r"^lppl[-_.].*", # LPPL-1.3c | 116 | r"^lppl[-_.].*", # LPPL-1.3c |
| 117 | - | 117 | + |
| 118 | # Microsoft Public License | 118 | # Microsoft Public License |
| 119 | r"^ms[-_]?pl$", # MS-PL, MSPL | 119 | 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-License | 122 | r"^mozilla[-_.].*", # Mozilla-Public-License |
| 123 | - | 123 | + |
| 124 | # OpenSSL License | 124 | # OpenSSL License |
| 125 | r"^openssl$", # OpenSSL.txt | 125 | r"^openssl$", # OpenSSL.txt |
| 126 | - | 126 | + |
| 127 | # PHP License | 127 | # PHP License |
| 128 | r"^php[-_.].*", # PHP-3.0, PHP-License | 128 | r"^php[-_.].*", # PHP-3.0, PHP-License |
| 129 | - | 129 | + |
| 130 | # Python License | 130 | # Python License |
| 131 | r"^python[-_.].*", # Python-2.0, Python-License | 131 | 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.1 | 134 | r"^sil[-_.].*", # SIL-OFL-1.1 |
| 135 | - | 135 | + |
| 136 | # Vim License | 136 | # Vim License |
| 137 | r"^vim$", # Vim.txt | 137 | r"^vim$", # Vim.txt |
| 138 | - | 138 | + |
| 139 | # W3C License | 139 | # W3C License |
| 140 | r"^w3c$", # W3C.txt | 140 | r"^w3c$", # W3C.txt |
| 141 | r"^w3c[-_.].*", # W3C-License | 141 | 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.txt | 144 | r"^wtfpl$", # WTFPL.txt |
| 145 | - | 145 | + |
| 146 | # Zope Public License | 146 | # Zope Public License |
| 147 | r"^zpl$", # ZPL.txt | 147 | r"^zpl$", # ZPL.txt |
| 148 | r"^zpl[-_.].*", # ZPL-2.1 | 148 | 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_dir | 66 | return extract_dir |
| @@ -12,7 +12,6 @@ | |||
| 12 | # See the License for the specific language governing permissions and | 12 | # 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 | ||
| 16 | import re | 15 | import re |
| 17 | from pathlib import Path | 16 | from pathlib import Path |
| 18 | from typing import Set, List, Dict, Any, Optional | 17 | from typing import Set, List, Dict, Any, Optional |
| @@ -24,19 +23,19 @@ from .config import LICENSE_FILE_PATTERNS, LICENSE_EXTENSIONS | |||
| 24 | def _is_exact_license_keyword(stem: str) -> bool: | 23 | def _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: | |||
| 54 | def is_license_file(filename: str) -> bool: | 53 | def 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 = None | 76 | 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 = pattern | 79 | matched_pattern = pattern |
| 81 | break | 80 | break |
| 82 | - | 81 | + |
| 83 | if not matched_pattern: | 82 | if not matched_pattern: |
| 84 | return False | 83 | 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 True | 88 | 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 False | 92 | return False |
| 94 | - | 93 | + |
| 95 | return True | 94 | return True |
| 96 | 95 | ||
| 97 | 96 | ||
| 98 | def is_license_directory(dirname: str) -> bool: | 97 | def 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: | |||
| 112 | def get_license_files_from_root(root_path: Path) -> Set[str]: | 111 | def 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_files | 127 | 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_files | 146 | 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 None | 175 | 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_name | 195 | 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_records | 199 | 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_path | 223 | 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_path | 253 | 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 or | 266 | license_name_lower == spdx_lower or |
| 268 | license_name_lower == license_lower or | 267 | license_name_lower == license_lower or |
| 269 | license_name_lower in spdx_lower or | 268 | license_name_lower in spdx_lower or |
| 270 | license_name_lower in license_lower | 269 | 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_filenames | 304 | 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_records | 310 | return license_records |
| 312 | 311 | ||
| 313 | 312 | ||
| 314 | def _get_best_matched_text(file_info: dict) -> str: | 313 | def _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 = 0 | 335 | best_length = 0 |
| 337 | best_match = None | 336 | best_match = None |
| 338 | has_license_rule = False | 337 | 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_text | 356 | best_text = matched_text |
| 358 | best_length = matched_length | 357 | best_length = matched_length |
| 359 | best_match = match | 358 | best_match = match |
| 360 | - | 359 | + |
| 361 | # 如果matched_text为空但有matched_length,说明JSON中没有存储matched_text | 360 | # 如果matched_text为空但有matched_length,说明JSON中没有存储matched_text |
| 362 | # 此时返回空字符串,调用者需要从源文件读取 | 361 | # 此时返回空字符串,调用者需要从源文件读取 |
| 363 | return best_text | 362 | return best_text |
| @@ -366,12 +365,12 @@ def _get_best_matched_text(file_info: dict) -> str: | |||
| 366 | def get_best_match_info(file_info: dict) -> Dict[str, Any]: | 365 | def 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 = 0 | 392 | best_length = 0 |
| 394 | best_match = None | 393 | best_match = None |
| 395 | has_license_rule = False | 394 | 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 = match | 411 | best_match = match |
| 413 | best_length = matched_length | 412 | 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]: | |||
| 433 | def _is_valid_license_match(file_info: dict) -> bool: | 432 | def _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 | continue | 490 | 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 | continue | 496 | 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 or | 504 | license_name_lower in license_lower or |
| 506 | spdx_lower in license_name_lower or | 505 | 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 = True | 512 | 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 | break | 518 | break |
| 520 | if not from_file_self: | 519 | if not from_file_self: |
| 521 | break | 520 | 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_records | 564 | matching_records = filtered_records |
| 566 | - | 565 | + |
| 567 | return matching_records | 566 | 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.MIT | 577 | 支持处理嵌套目录结构:用户传入 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记录,如果未找到或为误匹配则返回None | 595 | 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 = False | 606 | 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 = True | 610 | 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 = True | 613 | is_match = True |
| 615 | - | 614 | + |
| 616 | if not is_match: | 615 | if not is_match: |
| 617 | continue | 616 | 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 None | 623 | 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 None | 635 | return None |
| 637 | 636 | ||
| 638 | 637 | ||
| 639 | def _is_valid_license_path(file_path: str, target_license_files: Set[str]) -> bool: | 638 | def _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.txt | 650 | - 例如: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 True | 662 | 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).parts | 668 | 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 False | 674 | 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 False | 683 | return False |
| @@ -703,38 +702,38 @@ def _is_valid_license_path(file_path: str, target_license_files: Set[str]) -> bo | |||
| 703 | def _get_license_path_priority(file_path: str) -> int: | 702 | def _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 3 | 732 | 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 3 | 739 | return 3 |
| @@ -755,33 +754,33 @@ def _get_license_path_priority(file_path: str) -> int: | |||
| 755 | def _clean_scancode_path(file_path: str) -> str: | 754 | def _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.MIT | 759 | - 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_path | 775 | return file_path |
| 777 | - | 776 | + |
| 778 | # 查找并跳过 -extract 目录 | 777 | # 查找并跳过 -extract 目录 |
| 779 | start_idx = 0 | 778 | 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 + 1 | 781 | start_idx = i + 1 |
| 783 | break | 782 | 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 | continue | 823 | 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 | continue | 829 | 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 | continue | 833 | 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 | continue | 838 | 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 = True | 846 | from_file_self = True |
| @@ -853,7 +852,7 @@ def _extract_all_licenses( | |||
| 853 | break | 852 | break |
| 854 | if not from_file_self: | 853 | if not from_file_self: |
| 855 | break | 854 | 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 = key | 893 | 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_records | 896 | 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_records | 899 | 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 info | 84 | 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 results | 90 | 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 known | 96 | 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 results | 102 | 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 args | 252 | return args |
| 253 | 253 | ||
| 254 | 254 | ||
| 255 | def main(): | 255 | def 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.parent | 268 | 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 = None | 277 | extract_dir = None |
| 278 | scan_target = target | 278 | scan_target = target |
| 279 | - | 279 | + |
| 280 | # 定义清理函数 | 280 | # 定义清理函数 |
| 281 | def cleanup(): | 281 | def cleanup(): |
| 282 | """清理过程文件和临时目录""" | 282 | """清理过程文件和临时目录""" |
| @@ -293,24 +293,24 @@ def main(): | |||
| 293 | nonlocal extract_dir | 293 | 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.name | 301 | software_name = args.name |
| 302 | software_version = args.version | 302 | software_version = args.version |
| 303 | license_name = args.license_name | 303 | license_name = args.license_name |
| 304 | license_path = args.license_path | 304 | 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 | pass | 332 | 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_path | 339 | 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_path | 344 | 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 | ||
| 396 | if __name__ == "__main__": | 396 | if __name__ == "__main__": |
| 397 | - main() | 397 | + main() |
| @@ -12,7 +12,6 @@ | |||
| 12 | # See the License for the specific language governing permissions and | 12 | # 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 | ||
| 16 | from pathlib import Path | 15 | from pathlib import Path |
| 17 | 16 | ||
| 18 | from .logger import info | 17 | from .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 | continue | 51 | 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 | ||
| 15 | import subprocess | 15 | import subprocess |
| 16 | 16 | ||
| 17 | +from .config import EXPECTED_SCANCODE_VERSION | ||
| 17 | from .logger import ok, warn | 18 | from .logger import ok, warn |
| 18 | 19 | ||
| 19 | -# 期望的 scancode-toolkit 版本 | ||
| 20 | -EXPECTED_SCANCODE_VERSION = "32.5.0" | ||
| 21 | - | ||
| 22 | 20 | ||
| 23 | def check_scancode_available(): | 21 | def 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 Text | 95 | # 输出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 = False | 99 | 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.txt | 103 | # 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 | break | 122 | 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_text | 128 | # 如果文件读取失败,尝试使用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 | ||
| 148 | def _clean_license_path(license_file_path: str, root_path: Path) -> str: | 147 | def _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.name | 173 | 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 = 0 | 182 | 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 + 1 | 186 | start_idx = i + 1 |
| 188 | continue | 187 | 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: | |||
| 194 | def _get_possible_license_paths(root_path: Path, relative_path: str) -> List[Path]: | 193 | def _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 | pass | 221 | pass |
| 223 | - | 222 | + |
| 224 | # 注意:移除了递归查找逻辑,避免误匹配 | 223 | # 注意:移除了递归查找逻辑,避免误匹配 |
| 225 | # 例如:避免将 docs/LICENSE 或 third_party/lib/LICENSE 误匹配为根目录的 LICENSE | 224 | # 例如:避免将 docs/LICENSE 或 third_party/lib/LICENSE 误匹配为根目录的 LICENSE |
| 226 | - | 225 | + |
| 227 | return possible_paths | 226 | return possible_paths |