Skip to content

Commit d607932

Browse files
authored
build: correct Jieba release license metadata (#593)
1 parent 0f049de commit d607932

5 files changed

Lines changed: 257 additions & 24 deletions

File tree

.licenserc.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ header:
2828
- ".github/PULL_REQUEST_TEMPLATE.md"
2929
- "crates/paimon/tests/**/*.json"
3030
- "crates/paimon/testdata/**"
31+
- "third-party-licenses/jieba-rs-0.10.3.LICENSE"
3132
- "third-party-licenses/openssl-1.1.1.LICENSE"
3233
- "**/go.sum"
3334
- "**/DEPENDENCIES.*.tsv"

scripts/release_licenses.py

Lines changed: 78 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ class LicenseCorrection:
6161
license_path: str
6262
license_name: str
6363
anchor: str
64+
license_from_repository: bool = False
65+
expected_versions: tuple[tuple[str, str], ...] = ()
6466

6567

6668
@dataclass(frozen=True)
@@ -122,6 +124,18 @@ class BundledComponent:
122124
)
123125

124126
PYTHON_MIT_CORRECTIONS = (
127+
LicenseCorrection(
128+
crates=("jieba-macros", "jieba-rs"),
129+
license_crate="jieba-rs",
130+
license_path="third-party-licenses/jieba-rs-0.10.3.LICENSE",
131+
license_name="MIT License (jieba-rs workspace)",
132+
anchor="mit-jieba-rs-workspace",
133+
license_from_repository=True,
134+
expected_versions=(
135+
("jieba-macros", "0.10.3"),
136+
("jieba-rs", "0.10.3"),
137+
),
138+
),
125139
LicenseCorrection(
126140
crates=(
127141
"ownedbytes",
@@ -342,14 +356,18 @@ def resolved_packages(metadata: dict) -> list[dict]:
342356
]
343357

344358

345-
def package_by_name(
346-
metadata: dict, crate_name: str, required: bool = True
347-
) -> dict | None:
348-
matches = [
359+
def packages_by_name(metadata: dict, crate_name: str) -> list[dict]:
360+
return [
349361
package
350362
for package in resolved_packages(metadata)
351363
if package["name"] == crate_name
352364
]
365+
366+
367+
def package_by_name(
368+
metadata: dict, crate_name: str, required: bool = True
369+
) -> dict | None:
370+
matches = packages_by_name(metadata, crate_name)
353371
if not matches and not required:
354372
return None
355373
if len(matches) != 1:
@@ -371,25 +389,61 @@ def package_features(metadata: dict, package: dict) -> set[str]:
371389
return set(matches[0])
372390

373391

374-
def correction_html(metadata: dict, correction: LicenseCorrection) -> str:
392+
def correction_html(
393+
root: Path, metadata: dict, correction: LicenseCorrection
394+
) -> str:
395+
expected_versions = dict(correction.expected_versions)
375396
used_by = []
376397
for crate_name in correction.crates:
377-
package = package_by_name(metadata, crate_name)
378-
repository = package.get("repository") or (
379-
f"https://crates.io/crates/{crate_name}"
380-
)
381-
used_by.append(
382-
f' <li><a href="{html.escape(repository, quote=True)}">'
383-
f"{html.escape(crate_name)} {html.escape(package['version'])}</a></li>"
384-
)
398+
packages = packages_by_name(metadata, crate_name)
399+
if not packages:
400+
raise RuntimeError(f"expected at least one resolved {crate_name} package")
401+
for package in packages:
402+
if correction.license_from_repository:
403+
expected_version = expected_versions.get(crate_name)
404+
if expected_version is None:
405+
raise RuntimeError(
406+
"repository-backed correction is missing an expected "
407+
f"version for {crate_name}"
408+
)
409+
if package["version"] != expected_version:
410+
raise RuntimeError(
411+
f"expected {crate_name} {expected_version}, "
412+
f"found {package['version']}"
413+
)
414+
repository = package.get("repository") or (
415+
f"https://crates.io/crates/{crate_name}"
416+
)
417+
used_by.append(
418+
f' <li><a href="{html.escape(repository, quote=True)}">'
419+
f"{html.escape(crate_name)} {html.escape(package['version'])}</a></li>"
420+
)
385421

386-
license_package = package_by_name(metadata, correction.license_crate)
387-
license_file = (
388-
Path(license_package["manifest_path"]).parent / correction.license_path
389-
)
390-
if not license_file.is_file():
391-
raise RuntimeError(f"corrected license file is missing: {license_file}")
392-
license_text = license_file.read_text(encoding="utf-8")
422+
if correction.license_from_repository:
423+
license_file = root / correction.license_path
424+
if not license_file.is_file():
425+
raise RuntimeError(f"corrected license file is missing: {license_file}")
426+
license_text = license_file.read_text(encoding="utf-8")
427+
else:
428+
license_files = [
429+
Path(package["manifest_path"]).parent / correction.license_path
430+
for package in packages_by_name(metadata, correction.license_crate)
431+
]
432+
if not license_files:
433+
raise RuntimeError(
434+
f"expected at least one resolved {correction.license_crate} package"
435+
)
436+
missing = [path for path in license_files if not path.is_file()]
437+
if missing:
438+
raise RuntimeError(f"corrected license file is missing: {missing[0]}")
439+
license_texts = {
440+
path.read_text(encoding="utf-8") for path in license_files
441+
}
442+
if len(license_texts) != 1:
443+
raise RuntimeError(
444+
f"corrected license files differ for {correction.license_crate}"
445+
)
446+
license_text = license_texts.pop()
393447

394448
return "\n".join(
395449
[
@@ -407,6 +461,7 @@ def correction_html(metadata: dict, correction: LicenseCorrection) -> str:
407461

408462

409463
def replace_placeholder_entry(
464+
root: Path,
410465
report_text: str,
411466
metadata: dict,
412467
marker: str,
@@ -441,7 +496,7 @@ def replace_placeholder_entry(
441496
)
442497

443498
replacement = "\n".join(
444-
correction_html(metadata, correction) for correction in corrections
499+
correction_html(root, metadata, correction) for correction in corrections
445500
)
446501
return report_text[:entry_start] + replacement + report_text[entry_end:]
447502

@@ -538,13 +593,13 @@ def complete_report(
538593
root: Path, base_report: str, report: Report, metadata: dict
539594
) -> str:
540595
result = replace_placeholder_entry(
541-
base_report, metadata, ALLOC_PLACEHOLDER, ALLOC_CORRECTIONS
596+
root, base_report, metadata, ALLOC_PLACEHOLDER, ALLOC_CORRECTIONS
542597
)
543598
mit_corrections = COMMON_MIT_CORRECTIONS
544599
if report.component == "python":
545600
mit_corrections += PYTHON_MIT_CORRECTIONS
546601
result = replace_placeholder_entry(
547-
result, metadata, MIT_PLACEHOLDER, mit_corrections
602+
root, result, metadata, MIT_PLACEHOLDER, mit_corrections
548603
)
549604

550605
for placeholder in LICENSE_PLACEHOLDERS:

scripts/tests/test_release_licenses.py

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,156 @@
1919
import unittest
2020
from pathlib import Path
2121

22-
from scripts.release_licenses import binary_notice, resolved_packages
22+
from scripts.release_licenses import (
23+
LicenseCorrection,
24+
PYTHON_MIT_CORRECTIONS,
25+
binary_notice,
26+
correction_html,
27+
resolved_packages,
28+
)
29+
30+
31+
class LicenseCorrectionsTest(unittest.TestCase):
32+
def test_python_corrections_cover_jieba_workspace(self) -> None:
33+
corrections = [
34+
correction
35+
for correction in PYTHON_MIT_CORRECTIONS
36+
if correction.crates == ("jieba-macros", "jieba-rs")
37+
]
38+
self.assertEqual(len(corrections), 1)
39+
correction = corrections[0]
40+
self.assertEqual(correction.license_crate, "jieba-rs")
41+
self.assertEqual(
42+
correction.license_path,
43+
"third-party-licenses/jieba-rs-0.10.3.LICENSE",
44+
)
45+
root = Path(__file__).resolve().parents[2]
46+
self.assertTrue((root / correction.license_path).is_file())
47+
48+
def test_repository_license_correction_reads_checked_in_file(self) -> None:
49+
with tempfile.TemporaryDirectory() as temp:
50+
root = Path(temp)
51+
license_path = root / "third-party-licenses" / "jieba.LICENSE"
52+
license_path.parent.mkdir()
53+
license_path.write_text("Jieba workspace license\n", encoding="utf-8")
54+
metadata = {
55+
"normal_packages": {
56+
("jieba-macros", "0.10.3"),
57+
("jieba-rs", "0.10.3"),
58+
},
59+
"packages": [
60+
{
61+
"name": name,
62+
"version": "0.10.3",
63+
"manifest_path": str(root / name / "Cargo.toml"),
64+
"repository": "https://example.com/jieba-rs",
65+
}
66+
for name in ("jieba-macros", "jieba-rs")
67+
],
68+
}
69+
correction = LicenseCorrection(
70+
crates=("jieba-macros", "jieba-rs"),
71+
license_crate="jieba-rs",
72+
license_path="third-party-licenses/jieba.LICENSE",
73+
license_name="MIT License (jieba-rs workspace)",
74+
anchor="mit-jieba-rs-workspace",
75+
license_from_repository=True,
76+
expected_versions=(
77+
("jieba-macros", "0.10.3"),
78+
("jieba-rs", "0.10.3"),
79+
),
80+
)
81+
82+
result = correction_html(root, metadata, correction)
83+
84+
self.assertIn("Jieba workspace license", result)
85+
self.assertIn("jieba-macros 0.10.3", result)
86+
self.assertIn("jieba-rs 0.10.3", result)
87+
88+
def test_repository_license_correction_rejects_unverified_version(self) -> None:
89+
root = Path(__file__).resolve().parents[2]
90+
correction = next(
91+
correction
92+
for correction in PYTHON_MIT_CORRECTIONS
93+
if correction.crates == ("jieba-macros", "jieba-rs")
94+
)
95+
metadata = {
96+
"normal_packages": {
97+
("jieba-macros", "9.9.9"),
98+
("jieba-rs", "9.9.9"),
99+
},
100+
"packages": [
101+
{
102+
"name": name,
103+
"version": "9.9.9",
104+
"manifest_path": str(root / name / "Cargo.toml"),
105+
"repository": "https://example.com/jieba-rs",
106+
}
107+
for name in ("jieba-macros", "jieba-rs")
108+
],
109+
}
110+
111+
with self.assertRaisesRegex(
112+
RuntimeError,
113+
r"expected jieba-macros 0\.10\.3, found 9\.9\.9",
114+
):
115+
correction_html(root, metadata, correction)
116+
117+
def test_correction_supports_multiple_versions_with_same_license(self) -> None:
118+
with tempfile.TemporaryDirectory() as temp:
119+
root = Path(temp)
120+
packages = []
121+
normal_packages = set()
122+
for version in ("0.7.0", "0.11.0"):
123+
package_dir = root / f"tantivy-common-{version}"
124+
package_dir.mkdir()
125+
manifest = package_dir / "Cargo.toml"
126+
manifest.write_text("[package]\n", encoding="utf-8")
127+
(package_dir / "LICENSE").write_text(
128+
"Shared Tantivy license\n",
129+
encoding="utf-8",
130+
)
131+
packages.append(
132+
{
133+
"name": "tantivy-common",
134+
"version": version,
135+
"manifest_path": str(manifest),
136+
"repository": "https://example.com/tantivy",
137+
}
138+
)
139+
normal_packages.add(("tantivy-common", version))
140+
metadata = {
141+
"normal_packages": normal_packages,
142+
"packages": packages,
143+
}
144+
correction = LicenseCorrection(
145+
crates=("tantivy-common",),
146+
license_crate="tantivy-common",
147+
license_path="LICENSE",
148+
license_name="MIT License (Tantivy workspace)",
149+
anchor="mit-tantivy-workspace",
150+
)
151+
152+
result = correction_html(root, metadata, correction)
153+
154+
self.assertIn("tantivy-common 0.7.0", result)
155+
self.assertIn("tantivy-common 0.11.0", result)
156+
self.assertEqual(result.count("Shared Tantivy license"), 1)
157+
158+
def test_jieba_license_source_is_documented_and_header_exempt(self) -> None:
159+
root = Path(__file__).resolve().parents[2]
160+
license_path = "third-party-licenses/jieba-rs-0.10.3.LICENSE"
161+
license_config = (root / ".licenserc.yaml").read_text(encoding="utf-8")
162+
third_party_readme = (
163+
root / "third-party-licenses" / "README.md"
164+
).read_text(encoding="utf-8")
165+
166+
self.assertIn(f'"{license_path}"', license_config)
167+
self.assertIn(
168+
"messense/jieba-rs/blob/"
169+
"c62e0df1f9dcc2cc1e014711c5aa4561ae260538/LICENSE",
170+
third_party_readme,
171+
)
23172

24173

25174
class ResolvedPackagesTest(unittest.TestCase):

third-party-licenses/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,16 @@ OpenSSL 1.1.1w on aarch64 Linux. Both versions use the same license text.
2525
The XZ Utils 5.8.3 license is read from the locked `liblzma-sys` crate at
2626
`xz/COPYING.0BSD`.
2727

28+
`jieba-rs-0.10.3.LICENSE` is the license file from the root of the upstream
29+
`jieba-rs` workspace at commit
30+
`c62e0df1f9dcc2cc1e014711c5aa4561ae260538`. It covers `jieba-rs` 0.10.3 and
31+
`jieba-macros` 0.10.3.
32+
2833
Target-specific Python and Go reports are generated in their binary build jobs.
2934
They are intentionally not committed or included in the ASF source archive.
3035

3136
Sources:
3237

3338
- <https://github.com/openssl/openssl/blob/OpenSSL_1_1_1k/LICENSE>
3439
- <https://github.com/openssl/openssl/blob/OpenSSL_1_1_1w/LICENSE>
40+
- <https://github.com/messense/jieba-rs/blob/c62e0df1f9dcc2cc1e014711c5aa4561ae260538/LICENSE>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
MIT License
2+
3+
Copyright (c) 2018 - 2019 messense
4+
Copyright (c) 2019 Paul Meng
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy
7+
of this software and associated documentation files (the "Software"), to deal
8+
in the Software without restriction, including without limitation the rights
9+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
copies of the Software, and to permit persons to whom the Software is
11+
furnished to do so, subject to the following conditions:
12+
13+
The above copyright notice and this permission notice shall be included in all
14+
copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
SOFTWARE.

0 commit comments

Comments
 (0)