rangareddy commented on code in PR #857:
URL: https://github.com/apache/incubator-xtable/pull/857#discussion_r3718152430


##########
release/scripts/generate_shaded_license_metadata.py:
##########
@@ -471,7 +579,15 @@ def render_notice(module: pathlib.Path) -> str:
 
 
 def main() -> None:
+    missing_texts: dict[pathlib.Path, list[tuple[str, str, str]]] = {}
     for module in shade_modules():
+        if not (module / "target" / "dependency-tree-runtime.txt").exists():

Review Comment:
   Fixed, and the impact turned out to be wider than the abort.
   
   The version is now parsed as `parts[-2]`, with the classifier read only when 
the line has six fields, and a `tests` classifier excluded alongside `test-jar` 
(`org.apache.hbase:hbase-common:jar:tests:2.0.0-alpha4` was getting through, 
since its *type* is `jar`).
   
   Beyond the crash: there are ten classified coordinates across the four 
trees, and the nine that did not raise were being written into 
`LICENSE-bundled` with the **classifier in place of the version** — 
`io.netty:netty-transport-native-epoll:linux-aarch_64`, 
`org.apache.orc:orc-core:shaded-protobuf`, `org.apache.hive:hive-exec:core`. 
They survived only because an override short-circuited `normalize_family()` 
before the POM lookup, which means six of the `ARTIFACT_OVERRIDES` existed 
purely to mask this bug. All six are now gone (see the override thread).
   
   On the skip list: I was going to argue against it and the numbers changed my 
mind, so I have taken your suggestion. Covering `xtable-utilities` needs ~32 
curated license texts plus a family decision for 
`org.openjdk.jol:jol-core:0.16`, which is GPLv2-with-Classpath-Exception and 
deliberately shaded (`xtable-utilities/pom.xml:717`). For an artifact 
AGENTS.md:105 calls unpublished and not release-blocking, that is not rc2 work. 
`SKIPPED_SHADE_MODULES` now mirrors the validator's list with a comment saying 
why. I did still add the GPLv2+CPE family mapping and a `FAMILY_ORDER` entry, 
so the module can be un-skipped later without hitting an unmapped family.
   



##########
release/scripts/generate_shaded_license_metadata.py:
##########
@@ -471,7 +579,15 @@ def render_notice(module: pathlib.Path) -> str:
 
 
 def main() -> None:
+    missing_texts: dict[pathlib.Path, list[tuple[str, str, str]]] = {}
     for module in shade_modules():
+        if not (module / "target" / "dependency-tree-runtime.txt").exists():

Review Comment:
   Agreed, and made stricter than requested: rather than regenerating the 
modules that do have a tree and exiting non-zero at the end, the run now aborts 
**before writing anything**, so it is a genuine no-op. The error names the 
modules and prints the exact `./mvnw … dependency:tree` command to produce them.
   



##########
release/scripts/generate_shaded_license_metadata.py:
##########
@@ -447,6 +475,86 @@ def notice_text_for(group: str, artifact: str, version: 
str) -> str | None:
     return None
 
 
+@lru_cache(maxsize=None)
+def license_text_from_jar(group: str, artifact: str, version: str) -> str | 
None:
+    path = jar_path(group, artifact, version)
+    if not path.exists():
+        return None
+    with zipfile.ZipFile(path) as jar_file:
+        names = set(jar_file.namelist())
+        for candidate in JAR_LICENSE_CANDIDATES:
+            if candidate not in names:
+                continue
+            text = jar_file.read(candidate).decode("utf-8", errors="replace")
+            lines = [line.rstrip() for line in text.replace("\r\n", 
"\n").replace("\r", "\n").split("\n")]
+            normalized = "\n".join(lines).strip()
+            if normalized:
+                return normalized + "\n"
+    return None
+
+
+def license_text_for(group: str, artifact: str, version: str) -> str | None:
+    """Return the license text for a bundled dependency.
+
+    A curated override keyed by artifactId is consulted first: it is human
+    verified and is required for dependencies whose jar carries no license, or 
a
+    misleading one (e.g. junit bundles Hamcrest's BSD text rather than its own
+    Common Public License). Otherwise the dependency's own jar is 
authoritative.
+    Returns None when neither source has a text, so the caller can fail loudly
+    rather than ship an unattributed dependency.
+    """
+    override = OVERRIDES_DIR / f"LICENSE-{artifact}"
+    if override.exists():
+        content = override.read_text()
+        return content if content.endswith("\n") else content + "\n"
+
+    return license_text_from_jar(group, artifact, version)
+
+
+def bundled_non_apache_coords(license_path: pathlib.Path) -> list[tuple[str, 
str, str]]:
+    """Parse the non-Apache dependency coordinates out of a LICENSE-bundled 
file.
+
+    License texts are keyed off the committed LICENSE-bundled (the canonical 
list
+    of what the shaded jar bundles) rather than a freshly resolved dependency
+    tree, so the texts stay reproducible regardless of the local ~/.m2 state.
+    """
+    coords: list[tuple[str, str, str]] = []
+    family = None
+    lines = license_path.read_text().splitlines()
+    for index, line in enumerate(lines):
+        stripped = line.strip()
+        if not stripped:
+            family = None
+            continue
+        if index + 1 < len(lines) and re.fullmatch(r"-{3,}", lines[index + 
1].strip()):
+            family = stripped
+            continue
+        if re.fullmatch(r"-{3,}", stripped):
+            continue
+        if family and family != APACHE_FAMILY and stripped.count(":") == 2:
+            group, artifact, version = stripped.split(":")
+            coords.append((group, artifact, version))
+    return coords
+
+
+def sync_license_texts(module: pathlib.Path, coords: list[tuple[str, str, 
str]]) -> list[tuple[str, str, str]]:
+    """Regenerate META-INF/licenses/ for a module; return coords with no 
text."""
+    licenses_dir = module / "src" / "main" / "resources" / "META-INF" / 
"licenses"
+    licenses_dir.mkdir(parents=True, exist_ok=True)
+
+    for stale in licenses_dir.glob("LICENSE-*"):

Review Comment:
   Done, and at the whole-run level rather than per-module, which is the 
"ideally" version you described.
   
   - `plan_module()` computes everything a module would write and writes 
nothing.
   - `resolve_license_texts()` returns `(resolved, missing, mismatched)` with 
no `unlink()` anywhere near it.
   - `main()` plans every module first; only if all of them resolve cleanly 
does it write. `write_license_texts()` is now the only function that deletes, 
and it runs after all resolution.
   
   So `LICENSE-bundled`/`NOTICE-bundled` are deferred too, and a failing run 
leaves the tree byte-identical. Verified repeatedly while working through these 
comments: every failure left `git status` showing only my own source edits.
   



##########
release/scripts/generate_shaded_license_metadata.py:
##########
@@ -447,6 +475,86 @@ def notice_text_for(group: str, artifact: str, version: 
str) -> str | None:
     return None
 
 
+@lru_cache(maxsize=None)
+def license_text_from_jar(group: str, artifact: str, version: str) -> str | 
None:
+    path = jar_path(group, artifact, version)
+    if not path.exists():
+        return None
+    with zipfile.ZipFile(path) as jar_file:
+        names = set(jar_file.namelist())
+        for candidate in JAR_LICENSE_CANDIDATES:
+            if candidate not in names:
+                continue
+            text = jar_file.read(candidate).decode("utf-8", errors="replace")
+            lines = [line.rstrip() for line in text.replace("\r\n", 
"\n").replace("\r", "\n").split("\n")]
+            normalized = "\n".join(lines).strip()
+            if normalized:
+                return normalized + "\n"
+    return None
+
+
+def license_text_for(group: str, artifact: str, version: str) -> str | None:
+    """Return the license text for a bundled dependency.
+
+    A curated override keyed by artifactId is consulted first: it is human
+    verified and is required for dependencies whose jar carries no license, or 
a
+    misleading one (e.g. junit bundles Hamcrest's BSD text rather than its own
+    Common Public License). Otherwise the dependency's own jar is 
authoritative.
+    Returns None when neither source has a text, so the caller can fail loudly
+    rather than ship an unattributed dependency.
+    """
+    override = OVERRIDES_DIR / f"LICENSE-{artifact}"
+    if override.exists():
+        content = override.read_text()
+        return content if content.endswith("\n") else content + "\n"
+
+    return license_text_from_jar(group, artifact, version)
+
+
+def bundled_non_apache_coords(license_path: pathlib.Path) -> list[tuple[str, 
str, str]]:
+    """Parse the non-Apache dependency coordinates out of a LICENSE-bundled 
file.
+
+    License texts are keyed off the committed LICENSE-bundled (the canonical 
list
+    of what the shaded jar bundles) rather than a freshly resolved dependency
+    tree, so the texts stay reproducible regardless of the local ~/.m2 state.
+    """
+    coords: list[tuple[str, str, str]] = []
+    family = None
+    lines = license_path.read_text().splitlines()
+    for index, line in enumerate(lines):
+        stripped = line.strip()
+        if not stripped:
+            family = None
+            continue
+        if index + 1 < len(lines) and re.fullmatch(r"-{3,}", lines[index + 
1].strip()):
+            family = stripped
+            continue
+        if re.fullmatch(r"-{3,}", stripped):
+            continue
+        if family and family != APACHE_FAMILY and stripped.count(":") == 2:
+            group, artifact, version = stripped.split(":")
+            coords.append((group, artifact, version))
+    return coords
+
+
+def sync_license_texts(module: pathlib.Path, coords: list[tuple[str, str, 
str]]) -> list[tuple[str, str, str]]:
+    """Regenerate META-INF/licenses/ for a module; return coords with no 
text."""
+    licenses_dir = module / "src" / "main" / "resources" / "META-INF" / 
"licenses"
+    licenses_dir.mkdir(parents=True, exist_ok=True)
+
+    for stale in licenses_dir.glob("LICENSE-*"):

Review Comment:
   Reproduced exactly, down to the same five files — `LICENSE-activation`, 
`LICENSE-commons-el`, `LICENSE-mail`, `LICENSE-oro`, `LICENSE-protobuf-java`, 
with `LICENSE-bundled` and `NOTICE-bundled` rewritten. Thanks for the precise 
report; it made this easy to confirm.
   
   Fixed as described in the thread above: resolution now completes for every 
module before anything is written or unlinked, so a failed run is a no-op 
across all modules and the script is safe to re-run.
   



##########
release/scripts/generate_shaded_license_metadata.py:
##########
@@ -483,6 +599,21 @@ def main() -> None:
         license_path.write_text(render_license(groups))
         notice_path.write_text(render_notice(module))
 
+        missing = sync_license_texts(module, 
bundled_non_apache_coords(license_path))

Review Comment:
   The round-trip is gone — `plan_module()` takes the non-Apache coordinates 
straight from the `groups` dict, exactly as you wrote it.
   
   One deviation: I kept `bundled_non_apache_coords()` rather than deleting it, 
because your other comment asks for a single shared parser and the validator 
does have to read the committed file. It is no longer the generator's input 
path; it is now the one implementation, exposed as `--non-apache-artifact-ids` 
and called from `validate_shaded_license_coverage.sh` in place of the awk 
block. That way both comments are satisfied at once instead of trading one for 
the other, and the three parsers you counted are down to one — the `^-+$` vs 
`-{3,}` divergence you spotted can no longer happen.
   



##########
release/scripts/generate_shaded_license_metadata.py:
##########
@@ -447,6 +475,86 @@ def notice_text_for(group: str, artifact: str, version: 
str) -> str | None:
     return None
 
 
+@lru_cache(maxsize=None)
+def license_text_from_jar(group: str, artifact: str, version: str) -> str | 
None:
+    path = jar_path(group, artifact, version)
+    if not path.exists():
+        return None
+    with zipfile.ZipFile(path) as jar_file:
+        names = set(jar_file.namelist())
+        for candidate in JAR_LICENSE_CANDIDATES:
+            if candidate not in names:
+                continue
+            text = jar_file.read(candidate).decode("utf-8", errors="replace")

Review Comment:
   Switched to strict, and to answer your follow-up directly — **yes, it would 
have broken today, on two dependencies, and after the classified-artifact fix 
it no longer does.**
   
   I decoded all 363 embedded `LICENSE*`/`NOTICE*` entries reachable from the 
three release modules. Four artifacts carry non-UTF-8 bytes:
   
   | coordinate | entry | failure |
   |---|---|---|
   | `javax.activation:activation:1.1` | `META-INF/LICENSE.txt` | `0xe1` at 
2427 |
   | `javax.mail:mail:1.4.1` | `META-INF/LICENSE.txt` | `0xba` at 14369 |
   | `javax.transaction:jta:1.1`, `transaction-api:1.1` | 
`META-INF/LICENSE.txt` | `0xe1` at 2393 |
   | `org.apache.ant:ant:1.9.1`, `ant-launcher:1.9.1` | `META-INF/LICENSE.txt` 
| `0xae` at 11770 |
   
   Reachability per path:
   
   - `notice_text_for()` (all coordinates): **0** failures, so the pre-existing 
`errors="replace"` you flagged at line 470 was harmless in practice. It is 
strict now as well.
   - `license_text_from_jar()` (non-Apache only): the two `javax.*` ones. `jta` 
and `ant` normalise to Apache 2.0 and never reach it.
   - Once the bundled set is computed correctly, `javax.activation` and 
`javax.mail` are no longer bundled at all, so the current run has zero decode 
failures.
   
   Your instinct was right; the specific artifacts were 
`javax.activation`/`javax.mail` rather than jline/xmlenc/oro. Implemented as 
`_decode_entry()` raising `UndecodableText`: for license texts that routes the 
artifact onto the "needs a curated override" list, and NOTICE failures are 
collected and reported rather than silently mangled into U+FFFD.
   



##########
release/scripts/generate_shaded_license_metadata.py:
##########
@@ -277,6 +302,7 @@
     "javax.servlet": "Apache License 2.0",
     "javax.servlet.jsp": "Apache License 2.0",
     "javax.transaction": "Apache License 2.0",
+    "org.apache.hadoop": "Apache License 2.0",

Review Comment:
   Confirmed — you were right, it is not required, and it is now deleted. But 
it took closing two separate POM-parsing gaps to make that true, and neither 
was a `~/.m2` miss.
   
   **Gap 1 — a POM expat rejects.** `hadoop-yarn-api:3.1.0` inherits from 
`hadoop-yarn:3.1.0` → `hadoop-project:3.1.0`, and that file fails to parse at 
line 1787:
   
   ```xml
                   <Xlint:-unchecked/>
   ```
   
   `Xlint` is an undeclared namespace prefix and the local name starts with a 
hyphen. Maven's parser accepts it; expat rejects the whole file. The POM is 
authentic, not a corrupt download — SHA-1 matches its `.sha1` sidecar from 
Central (`f991e7f6d388e33a3fe83e18cfbf8815f7222e75`). Its own parent 
`hadoop-main:3.1.0` declares Apache 2.0 and parses fine, so the answer was 
always one hop away.
   
   **Gap 2 — the Maven namespace bound to a prefix.** 
`hadoop-yarn-client-2.7.1.pom` opens with
   
   ```xml
   <project xmlns:pom="http://maven.apache.org/POM/4.0.0";>
   ```
   
   so every element is unqualified and `./m:parent` / `./m:licenses` matched 
nothing — the chain looked empty. Also authentic per its SHA-1.
   
   Fixes: POM lookups now match on **local name** rather than a namespaced 
XPath, and `_lenient_pom_scan()` recovers the license names and parent 
coordinate by text scan when expat refuses a file. With both in place every 
hadoop coordinate resolves to Apache 2.0 from its own POM chain, so the 
override is gone.
   
   **On your second hypothesis** — that a resolution failure would be silently 
mislabelling other coordinates — it does not hold. `normalize_family()` ends in 
`raise ValueError("Unmapped license for …")`, and neither `__MISSING__` nor 
`__PARSE_ERROR__` matches any marker, so an unresolvable POM raises rather than 
defaulting to anything. The fail-loud behaviour you wanted is already there; 
the `hudi-io` crash was it working.
   
   **While I was in there**, I audited every override against a fresh 
resolution and dropped 18 that only repeated what the POM says. Each traces to 
one of the three bugs:
   
   | bug | overrides it made unnecessary |
   |---|---|
   | expat rejects the POM | `org.apache.hadoop` |
   | namespace not the default (12 bundled POMs hit this) | `aopalliance`, 
`it.unimi.dsi`, `javax.inject`, `org.codehaus.jackson`, `xml-apis`, `xmlenc` |
   | classifier read as version | `io.netty`, `org.apache.hbase`, and all six 
`ARTIFACT_OVERRIDES` |
   
   `xml-apis` was not merely redundant but **wrong**: it forced `Apache 
Software License 1.1` while both the POM and the jar's own `license/LICENSE` 
carry the Apache **2.0** text. `ARTIFACT_OVERRIDES` is down to one entry 
(`bcprov-jdk18on`, whose POM says "Bouncy Castle Licence" — the MIT text 
verbatim), with the reason in a comment.
   
   Three overrides I deliberately left alone, because they change ASF category 
and that is a legal call rather than a scripting one:
   
   | override | says | POM says |
   |---|---|---|
   | `javax.servlet` | Apache License 2.0 | CDDL + GPLv2 w/ CPE |
   | `javax.servlet.jsp` | Apache License 2.0 | CDDL + GPLv2 w/ CPE |
   | `stax` | CDDL + GPLv2 w/ CPE | Apache License 2.0 |
   
   The first two downgrade Category B to Category A, which suppresses the 
license text those dependencies would otherwise ship. Worth your second opinion.
   



##########
release/scripts/validate_shaded_license_coverage.sh:
##########
@@ -254,6 +254,38 @@ for module in "${SHADE_MODULES[@]}"; do
     printf '  - %s\n' "${unknown[@]}"
     overall_status=1
   fi
+
+  # Every dependency bundled under a non-Apache-2.0 license must ship its own
+  # license text under META-INF/licenses/LICENSE-<artifactId>. The Apache 
banner
+  # at the top of LICENSE-bundled only covers the Apache License 2.0 family.
+  licenses_dir="${module}/src/main/resources/META-INF/licenses"

Review Comment:
   Added, essentially as you wrote it, and verified that it fires:
   
   ```
   $ echo planted > 
xtable-aws/src/main/resources/META-INF/licenses/LICENSE-not-a-real-dep
   $ release/scripts/validate_shaded_license_coverage.sh
   FAIL xtable-aws: orphaned META-INF/licenses/ text with no bundled 
non-Apache-2.0 dependency:
     - LICENSE-not-a-real-dep
   ```
   
   The success line now reads "license texts match the bundled non-Apache-2.0 
dependencies **exactly**", since it asserts both directions. I hoisted the 
artifactId list into a variable as you suggested, though it now comes from the 
shared Python parser rather than the awk block (see the other thread).
   
   One correction to the framing, so nobody is misled later: the sets are not 
identical. A full regeneration removes nine orphans — `LICENSE-activation`, 
`asm`, `asm-commons`, `asm-tree`, `commons-el`, `mail`, `oro`, `protobuf-java`, 
`xml-apis` — whereas #866 removes `LICENSE-antlr4-runtime`, 
`LICENSE-slf4j-api`, `LICENSE-javax-annotation-api` and 
`LICENSE-glassfish-java-servlet-jsp`, which an earlier commit on this branch 
already removed. So #866 is genuinely subsumed and can be closed, but as 
"superseded", not "the same six files".
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to