This is an automated email from the ASF dual-hosted git repository.
FrankChen021 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git
The following commit(s) were added to refs/heads/master by this push:
new 98aa0498fef fix: license check accepts entries the release LICENSE
drops (#20707)
98aa0498fef is described below
commit 98aa0498fefb746e6bc8b31538eb174af478b8d1
Author: Andreas Maechler <[email protected]>
AuthorDate: Fri Oct 23 23:40:59 2026 -0600
fix: license check accepts entries the release LICENSE drops (#20707)
* fix: license check accepts entries the release LICENSE drops
The release build generates LICENSE.BINARY and NOTICE.BINARY without
licenses.yaml entries tagged extensions-contrib/ or
extensions/druid-testcontainers, but check-licenses.py only required an
entry under any module. A jar that ships in the release and is registered
only under a contrib module passed the check and was left out of the
release LICENSE.
check-licenses.py now takes the same --exclude-module-prefix flags as the
release generators and requires a dependency reported by a shipped module
to have an entry under a module the release keeps. It records every module
that reports or registers a dependency instead of keeping only the last
one, and maps report directories to licenses.yaml module tags. Extension
reports are now named after the artifactId so the tags match. The
missing-license rule has unit tests, which the apache-release build runs
before the check.
Registers net.java.dev.jna:jna-platform 5.19.1 under
extensions/druid-azure-extensions; it was registered only under
extensions-contrib/druid-ranger-security.
* Shorten the license check exclusion comment
---
.github/scripts/license_checks_script.sh | 3 +-
distribution/bin/check-licenses.py | 54 ++++++++++++++----
.../bin/generate-license-dependency-reports.py | 11 ++--
distribution/bin/test_check_licenses.py | 66 ++++++++++++++++++++++
distribution/pom.xml | 14 +++++
licenses.yaml | 10 ++++
6 files changed, 143 insertions(+), 15 deletions(-)
diff --git a/.github/scripts/license_checks_script.sh
b/.github/scripts/license_checks_script.sh
index 168b616750e..ce7adfb575b 100755
--- a/.github/scripts/license_checks_script.sh
+++ b/.github/scripts/license_checks_script.sh
@@ -24,4 +24,5 @@ set -e
# Generate dependency reports and checks they are valid.
mkdir -p target
distribution/bin/generate-license-dependency-reports.py . target
--clean-maven-artifact-transfer --parallel 2
-distribution/bin/check-licenses.py licenses.yaml target/license-reports
+# Same exclusions as the release LICENSE generation in distribution/pom.xml
+distribution/bin/check-licenses.py licenses.yaml target/license-reports
--exclude-module-prefix=extensions-contrib/
--exclude-module-prefix=extensions/druid-testcontainers
diff --git a/distribution/bin/check-licenses.py
b/distribution/bin/check-licenses.py
index 311a0c09579..0125778e57e 100755
--- a/distribution/bin/check-licenses.py
+++ b/distribution/bin/check-licenses.py
@@ -348,6 +348,7 @@ def get_version_string(version):
return str(version)
def find_druid_module_name(dirpath):
+ # Returns the licenses.yaml module tag of the report:
extensions/<artifactId> or java-core.
ext_start = dirpath.find("/ext/")
if ext_start > 0:
# Found an extension
@@ -356,10 +357,10 @@ def find_druid_module_name(dirpath):
if ext_name_end < 0:
raise Exception("Can't determine extension name from
[{}]".format(dirpath))
else:
- return subpath[0:ext_name_end]
+ return "extensions/" + subpath[0:ext_name_end]
else:
# Druid core
- return "core"
+ return "java-core"
def check_license_files(license_yaml, licenses_list):
# Every license_file_path must exist, and every file in licenses/bin must
be referenced by a license_file_path,
@@ -386,7 +387,27 @@ def check_license_files(license_yaml, licenses_list):
if missing_files or unreferenced_files:
sys.exit(1)
-def check_licenses(license_yaml, dependency_reports_root):
+def is_excluded_module(module, exclude_module_prefixes):
+ return any(module.startswith(prefix) for prefix in exclude_module_prefixes)
+
+def find_missing_licenses(reported_dep_to_licenses, reported_modules,
registered_modules, skipping_licenses, exclude_module_prefixes):
+ # A dependency that ships in the release needs at least one entry outside
the excluded modules.
+ missing_licenses = []
+ for key, reported_license_druid_module in reported_dep_to_licenses.items():
+ if reported_license_druid_module[0] == "-" or key in skipping_licenses:
+ continue
+ shipped_modules = sorted(m for m in reported_modules[key] if not
is_excluded_module(m, exclude_module_prefixes))
+ released = any(not is_excluded_module(m, exclude_module_prefixes) for
m in registered_modules.get(key, ()))
+ if key not in registered_modules:
+ modules = sorted(reported_modules[key])
+ elif shipped_modules and not released:
+ modules = shipped_modules
+ else:
+ continue
+ missing_licenses.append((", ".join(modules),) + key +
(reported_license_druid_module[0],))
+ return missing_licenses
+
+def check_licenses(license_yaml, dependency_reports_root,
exclude_module_prefixes):
# Build a dictionary to facilitate comparing reported licenses and
registered ones.
# These dictionaries are the mapping of (group_id, artifact_id, version)
to license_name.
@@ -396,6 +417,7 @@ def check_licenses(license_yaml, dependency_reports_root):
# Build reported license dictionary.
reported_dep_to_licenses = {}
+ reported_modules = {}
compatible_license_names = build_compatible_license_names()
for dirpath, dirnames, filenames in os.walk(dependency_reports_root):
for filename in filenames:
@@ -406,7 +428,10 @@ def check_licenses(license_yaml, dependency_reports_root):
print_log_to_stderr("Parsing {}".format(full_path))
with open(full_path, encoding="utf-8") as report_file:
parser = DependencyReportParser(druid_module_name,
compatible_license_names)
- reported_dep_to_licenses.update(parser.parse(report_file))
+ dep_to_license = parser.parse(report_file)
+ reported_dep_to_licenses.update(dep_to_license)
+ for key in dep_to_license:
+ reported_modules.setdefault(key,
set()).add(druid_module_name)
if len(reported_dep_to_licenses) == 0:
raise Exception("No dependency reports are found")
@@ -415,6 +440,7 @@ def check_licenses(license_yaml, dependency_reports_root):
# Build registered license dictionary.
registered_dep_to_licenses = {}
+ registered_modules = {}
skipping_licenses = {}
# Validate that every entry uses a known module tag. Core extensions use
extensions/<output-dir>
# (matching how they appear in the binary distribution's extensions/
directory), and contrib
@@ -469,14 +495,15 @@ def check_licenses(license_yaml, dependency_reports_root):
version = get_version_string(license['version'])
skipping_licenses[get_dep_key(group_id, artifact_id,
version)] = license
else:
- registered_dep_to_licenses[get_dep_key(group_id,
artifact_id, get_version_string(license['version']))] =
compatible_license_names[license['license_name']]
+ key = get_dep_key(group_id, artifact_id,
get_version_string(license['version']))
+ registered_dep_to_licenses[key] =
compatible_license_names[license['license_name']]
+ registered_modules.setdefault(key,
set()).add(license['module'])
if len(registered_dep_to_licenses) == 0:
raise Exception("No registered licenses are found")
# Compare licenses in registry and those in dependency reports.
mismatched_licenses = []
- missing_licenses = []
unchecked_licenses = []
# Iterate through registered licenses and check if its license is same
with the reported one.
for key, registered_license in registered_dep_to_licenses.items():
@@ -498,9 +525,9 @@ def check_licenses(license_yaml, dependency_reports_root):
print_log_to_stderr("")
# Let's find missing licenses, which are reported but missing in the
registry.
- for key, reported_license_druid_module in reported_dep_to_licenses.items():
- if reported_license_druid_module[0] != "-" and key not in
registered_dep_to_licenses and key not in skipping_licenses:
- missing_licenses.append((reported_license_druid_module[1], key[0],
key[1], key[2], reported_license_druid_module[0]))
+ missing_licenses = find_missing_licenses(
+ reported_dep_to_licenses, reported_modules, registered_modules,
skipping_licenses, exclude_module_prefixes
+ )
if len(missing_licenses) > 0:
print_log_to_stderr("Error: found {} missing licenses. These licenses
are reported, but missing in the registry".format(len(missing_licenses)))
@@ -534,11 +561,18 @@ if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Check and generate
license file.')
parser.add_argument('license_yaml', metavar='<path to license.yaml>',
type=str)
parser.add_argument('dependency_reports_root', metavar='<root to maven
dependency reports>', type=str)
+ parser.add_argument(
+ '--exclude-module-prefix',
+ action='append',
+ default=[],
+ help='Module prefix whose licenses.yaml entries the release
LICENSE omits. Repeatable. A dependency '
+ 'that ships in the release must be registered under at least
one module not matching these.'
+ )
args = parser.parse_args()
license_yaml = args.license_yaml
dependency_reports_root = args.dependency_reports_root
- check_licenses(license_yaml, dependency_reports_root)
+ check_licenses(license_yaml, dependency_reports_root,
args.exclude_module_prefix)
except KeyboardInterrupt:
print('Interrupted, closing.')
diff --git a/distribution/bin/generate-license-dependency-reports.py
b/distribution/bin/generate-license-dependency-reports.py
index 52577ea8111..28cc14f6184 100755
--- a/distribution/bin/generate-license-dependency-reports.py
+++ b/distribution/bin/generate-license-dependency-reports.py
@@ -59,17 +59,20 @@ def generate_reports(druid_path, tmp_path, exclude_ext,
num_threads):
if not exclude_ext:
extensions_core_path = os.path.join(druid_path, "extensions-core")
- command = "mvn -Dexec.executable='echo' -Dexec.args='${basedir}'
exec:exec -q | grep extensions-core | grep -o '[^/]*$'"
+ command = "mvn -Dexec.executable='echo' -Dexec.args='${basedir}
${project.artifactId}' exec:exec -q | grep extensions-core | grep -o '[^/]*$'"
extension_dirs = subprocess.check_output(command, cwd=druid_path,
shell=True).decode().split('\n')[:-1]
print("Found {} extensions".format(len(extension_dirs)))
- for extension_dir in extension_dirs:
- print("extension dir: {}".format(extension_dir))
+ for extension_dir_and_artifact_id in extension_dirs:
+ extension_dir, artifact_id = extension_dir_and_artifact_id.split('
')
+ print("extension dir: {}, artifactId: {}".format(extension_dir,
artifact_id))
extension_path = os.path.join(extensions_core_path, extension_dir)
if not os.path.isdir(extension_path):
print("{} is not a directory".format(extension_path))
continue
- extension_report_dir = "{}/{}".format(license_ext_path,
extension_dir)
+ # Name the report after the artifactId, which is the extension's
directory in the binary distribution
+ # and the module tag check-licenses.py matches against
licenses.yaml (extensions/<artifactId>).
+ extension_report_dir = "{}/{}".format(license_ext_path,
artifact_id)
script_args.append((extension_path, os.path.join(extension_path,
"target", "reports"), extension_report_dir))
print("Generating dependency reports")
diff --git a/distribution/bin/test_check_licenses.py
b/distribution/bin/test_check_licenses.py
new file mode 100755
index 00000000000..7583f41e1d2
--- /dev/null
+++ b/distribution/bin/test_check_licenses.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python3
+
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import importlib.util
+import os
+import unittest
+
+spec = importlib.util.spec_from_file_location(
+ "check_licenses", os.path.join(os.path.dirname(os.path.abspath(__file__)),
"check-licenses.py")
+)
+check_licenses = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(check_licenses)
+
+APACHE = "Apache License version 2.0"
+AZURE = "extensions/druid-azure-extensions"
+TESTCONTAINERS = "extensions/druid-testcontainers"
+RANGER = "extensions-contrib/druid-ranger-security"
+EXCLUDED = ["extensions-contrib/", TESTCONTAINERS]
+GAV = check_licenses.get_dep_key("org.example", "a", "1.0")
+
+
+def find_missing(reporting_modules, registered_modules, skipping=None):
+ reported = {GAV: (APACHE, reporting_modules[0])}
+ registered = {GAV: set(registered_modules)} if registered_modules else {}
+ return check_licenses.find_missing_licenses(reported, {GAV:
set(reporting_modules)}, registered, skipping or {}, EXCLUDED)
+
+
+class FindMissingLicensesTest(unittest.TestCase):
+ def test_registered_under_released_module(self):
+ self.assertEqual([], find_missing([AZURE], ["java-core"]))
+
+ def test_unregistered_is_missing(self):
+ self.assertEqual([(AZURE,) + GAV + (APACHE,)], find_missing([AZURE],
[]))
+
+ def
test_registered_only_under_excluded_module_is_missing_when_shipped(self):
+ self.assertEqual([(AZURE,) + GAV + (APACHE,)],
find_missing([TESTCONTAINERS, AZURE], [RANGER]))
+
+ def
test_registered_only_under_excluded_module_is_fine_when_not_shipped(self):
+ self.assertEqual([], find_missing([TESTCONTAINERS], [TESTCONTAINERS]))
+
+ def test_skipped_dependency_is_not_missing(self):
+ self.assertEqual([], find_missing([AZURE], [], skipping={GAV: {}}))
+
+
+class FindDruidModuleNameTest(unittest.TestCase):
+ def test_maps_report_dirs_to_module_tags(self):
+ self.assertEqual("java-core",
check_licenses.find_druid_module_name("target/license-reports/core/reports"))
+ self.assertEqual(AZURE,
check_licenses.find_druid_module_name("target/license-reports/ext/druid-azure-extensions/reports"))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/distribution/pom.xml b/distribution/pom.xml
index 77b8f358ab9..7202d8910f8 100644
--- a/distribution/pom.xml
+++ b/distribution/pom.xml
@@ -318,6 +318,16 @@
</arguments>
</configuration>
</execution>
+ <execution>
+ <id>test-check-licenses</id>
+ <phase>test</phase>
+ <goals>
+ <goal>exec</goal>
+ </goals>
+ <configuration>
+
<executable>${project.basedir}/bin/test_check_licenses.py</executable>
+ </configuration>
+ </execution>
<execution>
<id>check-licenses</id>
<phase>test</phase>
@@ -329,6 +339,10 @@
<arguments>
<argument>${project.parent.basedir}/licenses.yaml</argument>
<argument>${project.basedir}/target/license-reports</argument>
+ <!-- Keep these under
bundle-contrib-exts too: the check guards the Apache release
+ LICENSE, which leaves out contrib
entries. -->
+
<argument>--exclude-module-prefix=extensions-contrib/</argument>
+
<argument>--exclude-module-prefix=extensions/druid-testcontainers</argument>
</arguments>
</configuration>
</execution>
diff --git a/licenses.yaml b/licenses.yaml
index 8278a73faf8..d169fd88b60 100644
--- a/licenses.yaml
+++ b/licenses.yaml
@@ -5247,6 +5247,16 @@ libraries:
---
+name: net.java.dev.jna jna-platform
+license_category: binary
+version: 5.19.1
+module: extensions/druid-azure-extensions
+license_name: Apache License version 2.0
+libraries:
+ - net.java.dev.jna: jna-platform
+
+---
+
name: JOpt Simple
license_category: binary
version: 5.0.2
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]