diff --git a/.licenserc.yaml b/.licenserc.yaml
index c3cac61b..e147040a 100644
--- a/.licenserc.yaml
+++ b/.licenserc.yaml
@@ -28,6 +28,7 @@ header:
- ".github/PULL_REQUEST_TEMPLATE.md"
- "crates/paimon/tests/**/*.json"
- "crates/paimon/testdata/**"
+ - "third-party-licenses/jieba-rs-0.10.3.LICENSE"
- "third-party-licenses/openssl-1.1.1.LICENSE"
- "**/go.sum"
- "**/DEPENDENCIES.*.tsv"
diff --git a/scripts/release_licenses.py b/scripts/release_licenses.py
index 8c6e6323..5785cf80 100644
--- a/scripts/release_licenses.py
+++ b/scripts/release_licenses.py
@@ -61,6 +61,8 @@ class LicenseCorrection:
license_path: str
license_name: str
anchor: str
+ license_from_repository: bool = False
+ expected_versions: tuple[tuple[str, str], ...] = ()
@dataclass(frozen=True)
@@ -122,6 +124,18 @@ class BundledComponent:
)
PYTHON_MIT_CORRECTIONS = (
+ LicenseCorrection(
+ crates=("jieba-macros", "jieba-rs"),
+ license_crate="jieba-rs",
+ license_path="third-party-licenses/jieba-rs-0.10.3.LICENSE",
+ license_name="MIT License (jieba-rs workspace)",
+ anchor="mit-jieba-rs-workspace",
+ license_from_repository=True,
+ expected_versions=(
+ ("jieba-macros", "0.10.3"),
+ ("jieba-rs", "0.10.3"),
+ ),
+ ),
LicenseCorrection(
crates=(
"ownedbytes",
@@ -342,14 +356,18 @@ def resolved_packages(metadata: dict) -> list[dict]:
]
-def package_by_name(
- metadata: dict, crate_name: str, required: bool = True
-) -> dict | None:
- matches = [
+def packages_by_name(metadata: dict, crate_name: str) -> list[dict]:
+ return [
package
for package in resolved_packages(metadata)
if package["name"] == crate_name
]
+
+
+def package_by_name(
+ metadata: dict, crate_name: str, required: bool = True
+) -> dict | None:
+ matches = packages_by_name(metadata, crate_name)
if not matches and not required:
return None
if len(matches) != 1:
@@ -371,25 +389,61 @@ def package_features(metadata: dict, package: dict) -> set[str]:
return set(matches[0])
-def correction_html(metadata: dict, correction: LicenseCorrection) -> str:
+def correction_html(
+ root: Path, metadata: dict, correction: LicenseCorrection
+) -> str:
+ expected_versions = dict(correction.expected_versions)
used_by = []
for crate_name in correction.crates:
- package = package_by_name(metadata, crate_name)
- repository = package.get("repository") or (
- f"https://crates.io/crates/{crate_name}"
- )
- used_by.append(
- f'
'
- f"{html.escape(crate_name)} {html.escape(package['version'])}"
- )
+ packages = packages_by_name(metadata, crate_name)
+ if not packages:
+ raise RuntimeError(f"expected at least one resolved {crate_name} package")
+ for package in packages:
+ if correction.license_from_repository:
+ expected_version = expected_versions.get(crate_name)
+ if expected_version is None:
+ raise RuntimeError(
+ "repository-backed correction is missing an expected "
+ f"version for {crate_name}"
+ )
+ if package["version"] != expected_version:
+ raise RuntimeError(
+ f"expected {crate_name} {expected_version}, "
+ f"found {package['version']}"
+ )
+ repository = package.get("repository") or (
+ f"https://crates.io/crates/{crate_name}"
+ )
+ used_by.append(
+ f' '
+ f"{html.escape(crate_name)} {html.escape(package['version'])}"
+ )
- license_package = package_by_name(metadata, correction.license_crate)
- license_file = (
- Path(license_package["manifest_path"]).parent / correction.license_path
- )
- if not license_file.is_file():
- raise RuntimeError(f"corrected license file is missing: {license_file}")
- license_text = license_file.read_text(encoding="utf-8")
+ if correction.license_from_repository:
+ license_file = root / correction.license_path
+ if not license_file.is_file():
+ raise RuntimeError(f"corrected license file is missing: {license_file}")
+ license_text = license_file.read_text(encoding="utf-8")
+ else:
+ license_files = [
+ Path(package["manifest_path"]).parent / correction.license_path
+ for package in packages_by_name(metadata, correction.license_crate)
+ ]
+ if not license_files:
+ raise RuntimeError(
+ f"expected at least one resolved {correction.license_crate} package"
+ )
+ missing = [path for path in license_files if not path.is_file()]
+ if missing:
+ raise RuntimeError(f"corrected license file is missing: {missing[0]}")
+ license_texts = {
+ path.read_text(encoding="utf-8") for path in license_files
+ }
+ if len(license_texts) != 1:
+ raise RuntimeError(
+ f"corrected license files differ for {correction.license_crate}"
+ )
+ license_text = license_texts.pop()
return "\n".join(
[
@@ -407,6 +461,7 @@ def correction_html(metadata: dict, correction: LicenseCorrection) -> str:
def replace_placeholder_entry(
+ root: Path,
report_text: str,
metadata: dict,
marker: str,
@@ -441,7 +496,7 @@ def replace_placeholder_entry(
)
replacement = "\n".join(
- correction_html(metadata, correction) for correction in corrections
+ correction_html(root, metadata, correction) for correction in corrections
)
return report_text[:entry_start] + replacement + report_text[entry_end:]
@@ -538,13 +593,13 @@ def complete_report(
root: Path, base_report: str, report: Report, metadata: dict
) -> str:
result = replace_placeholder_entry(
- base_report, metadata, ALLOC_PLACEHOLDER, ALLOC_CORRECTIONS
+ root, base_report, metadata, ALLOC_PLACEHOLDER, ALLOC_CORRECTIONS
)
mit_corrections = COMMON_MIT_CORRECTIONS
if report.component == "python":
mit_corrections += PYTHON_MIT_CORRECTIONS
result = replace_placeholder_entry(
- result, metadata, MIT_PLACEHOLDER, mit_corrections
+ root, result, metadata, MIT_PLACEHOLDER, mit_corrections
)
for placeholder in LICENSE_PLACEHOLDERS:
diff --git a/scripts/tests/test_release_licenses.py b/scripts/tests/test_release_licenses.py
index 1742030e..2cfe1b8a 100644
--- a/scripts/tests/test_release_licenses.py
+++ b/scripts/tests/test_release_licenses.py
@@ -19,7 +19,156 @@
import unittest
from pathlib import Path
-from scripts.release_licenses import binary_notice, resolved_packages
+from scripts.release_licenses import (
+ LicenseCorrection,
+ PYTHON_MIT_CORRECTIONS,
+ binary_notice,
+ correction_html,
+ resolved_packages,
+)
+
+
+class LicenseCorrectionsTest(unittest.TestCase):
+ def test_python_corrections_cover_jieba_workspace(self) -> None:
+ corrections = [
+ correction
+ for correction in PYTHON_MIT_CORRECTIONS
+ if correction.crates == ("jieba-macros", "jieba-rs")
+ ]
+ self.assertEqual(len(corrections), 1)
+ correction = corrections[0]
+ self.assertEqual(correction.license_crate, "jieba-rs")
+ self.assertEqual(
+ correction.license_path,
+ "third-party-licenses/jieba-rs-0.10.3.LICENSE",
+ )
+ root = Path(__file__).resolve().parents[2]
+ self.assertTrue((root / correction.license_path).is_file())
+
+ def test_repository_license_correction_reads_checked_in_file(self) -> None:
+ with tempfile.TemporaryDirectory() as temp:
+ root = Path(temp)
+ license_path = root / "third-party-licenses" / "jieba.LICENSE"
+ license_path.parent.mkdir()
+ license_path.write_text("Jieba workspace license\n", encoding="utf-8")
+ metadata = {
+ "normal_packages": {
+ ("jieba-macros", "0.10.3"),
+ ("jieba-rs", "0.10.3"),
+ },
+ "packages": [
+ {
+ "name": name,
+ "version": "0.10.3",
+ "manifest_path": str(root / name / "Cargo.toml"),
+ "repository": "https://example.com/jieba-rs",
+ }
+ for name in ("jieba-macros", "jieba-rs")
+ ],
+ }
+ correction = LicenseCorrection(
+ crates=("jieba-macros", "jieba-rs"),
+ license_crate="jieba-rs",
+ license_path="third-party-licenses/jieba.LICENSE",
+ license_name="MIT License (jieba-rs workspace)",
+ anchor="mit-jieba-rs-workspace",
+ license_from_repository=True,
+ expected_versions=(
+ ("jieba-macros", "0.10.3"),
+ ("jieba-rs", "0.10.3"),
+ ),
+ )
+
+ result = correction_html(root, metadata, correction)
+
+ self.assertIn("Jieba workspace license", result)
+ self.assertIn("jieba-macros 0.10.3", result)
+ self.assertIn("jieba-rs 0.10.3", result)
+
+ def test_repository_license_correction_rejects_unverified_version(self) -> None:
+ root = Path(__file__).resolve().parents[2]
+ correction = next(
+ correction
+ for correction in PYTHON_MIT_CORRECTIONS
+ if correction.crates == ("jieba-macros", "jieba-rs")
+ )
+ metadata = {
+ "normal_packages": {
+ ("jieba-macros", "9.9.9"),
+ ("jieba-rs", "9.9.9"),
+ },
+ "packages": [
+ {
+ "name": name,
+ "version": "9.9.9",
+ "manifest_path": str(root / name / "Cargo.toml"),
+ "repository": "https://example.com/jieba-rs",
+ }
+ for name in ("jieba-macros", "jieba-rs")
+ ],
+ }
+
+ with self.assertRaisesRegex(
+ RuntimeError,
+ r"expected jieba-macros 0\.10\.3, found 9\.9\.9",
+ ):
+ correction_html(root, metadata, correction)
+
+ def test_correction_supports_multiple_versions_with_same_license(self) -> None:
+ with tempfile.TemporaryDirectory() as temp:
+ root = Path(temp)
+ packages = []
+ normal_packages = set()
+ for version in ("0.7.0", "0.11.0"):
+ package_dir = root / f"tantivy-common-{version}"
+ package_dir.mkdir()
+ manifest = package_dir / "Cargo.toml"
+ manifest.write_text("[package]\n", encoding="utf-8")
+ (package_dir / "LICENSE").write_text(
+ "Shared Tantivy license\n",
+ encoding="utf-8",
+ )
+ packages.append(
+ {
+ "name": "tantivy-common",
+ "version": version,
+ "manifest_path": str(manifest),
+ "repository": "https://example.com/tantivy",
+ }
+ )
+ normal_packages.add(("tantivy-common", version))
+ metadata = {
+ "normal_packages": normal_packages,
+ "packages": packages,
+ }
+ correction = LicenseCorrection(
+ crates=("tantivy-common",),
+ license_crate="tantivy-common",
+ license_path="LICENSE",
+ license_name="MIT License (Tantivy workspace)",
+ anchor="mit-tantivy-workspace",
+ )
+
+ result = correction_html(root, metadata, correction)
+
+ self.assertIn("tantivy-common 0.7.0", result)
+ self.assertIn("tantivy-common 0.11.0", result)
+ self.assertEqual(result.count("Shared Tantivy license"), 1)
+
+ def test_jieba_license_source_is_documented_and_header_exempt(self) -> None:
+ root = Path(__file__).resolve().parents[2]
+ license_path = "third-party-licenses/jieba-rs-0.10.3.LICENSE"
+ license_config = (root / ".licenserc.yaml").read_text(encoding="utf-8")
+ third_party_readme = (
+ root / "third-party-licenses" / "README.md"
+ ).read_text(encoding="utf-8")
+
+ self.assertIn(f'"{license_path}"', license_config)
+ self.assertIn(
+ "messense/jieba-rs/blob/"
+ "c62e0df1f9dcc2cc1e014711c5aa4561ae260538/LICENSE",
+ third_party_readme,
+ )
class ResolvedPackagesTest(unittest.TestCase):
diff --git a/third-party-licenses/README.md b/third-party-licenses/README.md
index 642d169a..449c9aa3 100644
--- a/third-party-licenses/README.md
+++ b/third-party-licenses/README.md
@@ -25,6 +25,11 @@ OpenSSL 1.1.1w on aarch64 Linux. Both versions use the same license text.
The XZ Utils 5.8.3 license is read from the locked `liblzma-sys` crate at
`xz/COPYING.0BSD`.
+`jieba-rs-0.10.3.LICENSE` is the license file from the root of the upstream
+`jieba-rs` workspace at commit
+`c62e0df1f9dcc2cc1e014711c5aa4561ae260538`. It covers `jieba-rs` 0.10.3 and
+`jieba-macros` 0.10.3.
+
Target-specific Python and Go reports are generated in their binary build jobs.
They are intentionally not committed or included in the ASF source archive.
@@ -32,3 +37,4 @@ Sources:
-
-
+-
diff --git a/third-party-licenses/jieba-rs-0.10.3.LICENSE b/third-party-licenses/jieba-rs-0.10.3.LICENSE
new file mode 100644
index 00000000..a9c2c612
--- /dev/null
+++ b/third-party-licenses/jieba-rs-0.10.3.LICENSE
@@ -0,0 +1,22 @@
+MIT License
+
+Copyright (c) 2018 - 2019 messense
+Copyright (c) 2019 Paul Meng
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.