diff --git a/.bazelrc b/.bazelrc index b98fc097..03a64397 100644 --- a/.bazelrc +++ b/.bazelrc @@ -1 +1,6 @@ test --test_output=errors + +build --java_language_version=17 +build --tool_java_language_version=17 +build --java_runtime_version=remotejdk_17 +build --tool_java_runtime_version=remotejdk_17 diff --git a/.github/workflows/license_check.yml b/.github/workflows/license_check.yml new file mode 100644 index 00000000..fdf6b483 --- /dev/null +++ b/.github/workflows/license_check.yml @@ -0,0 +1,30 @@ +# ******************************************************************************* +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +name: License Check +on: + pull_request: + types: [opened, reopened, synchronize] + merge_group: + types: [checks_requested] +jobs: + license-check: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4.2.2 + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.9.1 + - name: Run license checks + run: | + bazel run //:license.check.python diff --git a/BUILD b/BUILD index b423cbc9..70b73dee 100644 --- a/BUILD +++ b/BUILD @@ -12,6 +12,7 @@ # ******************************************************************************* load("//tools/cr_checker:cr_checker.bzl", "copyright_checker") +load("//tools/dash:dash.bzl", "dash_license_checker") test_suite( name = "format.check", @@ -35,6 +36,12 @@ copyright_checker( visibility = ["//visibility:public"], ) +dash_license_checker( + name = "python", + src = "//docs:requirements_lock", + visibility = ["//visibility:public"], +) + exports_files([ "MODULE.bazel", "BUILD", diff --git a/MODULE.bazel b/MODULE.bazel index 5a8d4229..7c51e8b3 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -71,3 +71,27 @@ bazel_dep(name = "buildifier_prebuilt", version = "7.3.1") # ############################################################################### bazel_dep(name = "aspect_rules_lint", version = "1.0.3") + +############################################################################### +# +# Java version +# +############################################################################### +bazel_dep(name = "rules_java", version = "8.6.3") + +############################################################################### +# +# HTTP Jar rule deps +# +############################################################################### +http_jar = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_jar") + +DASH_VERSION = "1.1.0" + +http_jar( + name = "dash_license_tool", + sha256 = "ba4e84e1981f0e51f92a42ec8fc8b9668dabb08d1279afde46b8414e11752b06", + urls = [ + "https://repo.eclipse.org/content/repositories/dash-licenses/org/eclipse/dash/org.eclipse.dash.licenses/{version}/org.eclipse.dash.licenses-{version}.jar".format(version = DASH_VERSION), + ], +) diff --git a/docs/BUILD b/docs/BUILD index 2ce1e3c3..abbfa926 100644 --- a/docs/BUILD +++ b/docs/BUILD @@ -133,3 +133,12 @@ py_venv( # Until release of esbonio 1.x, we need to install it ourselves so the VS Code extension can find it. deps = sphinx_requirements + [requirement("esbonio")], ) + +# Needed for Dash tool to check python dependency licenses. +filegroup( + name = "requirements_lock", + srcs = [ + "_tooling/requirements_lock.txt", + ], + visibility = ["//visibility:public"], +) diff --git a/tools/dash/BUILD b/tools/dash/BUILD new file mode 100644 index 00000000..e69de29b diff --git a/tools/dash/dash.bzl b/tools/dash/dash.bzl new file mode 100644 index 00000000..23e02604 --- /dev/null +++ b/tools/dash/dash.bzl @@ -0,0 +1,55 @@ +# ******************************************************************************* +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@rules_java//java:java_binary.bzl", "java_binary") +load("//tools/dash/formatters:dash_format_converter.bzl", "dash_format_converter") + +def dash_license_checker( + name, + src, + visibility): + """ + Defines a Bazel macro for creating a `java_binary` target that integrates the DASH license checker. + + Args: + name (str): + The name of the `java_binary` target to be created. This will serve as the identifier + for the generated Bazel target. + src (str): + The path to the dependency list file required by the DASH license checker. + This file should specify the dependencies to be validated. + visibility (list[str]): + A list defining the visibility of the created target. It determines which packages + can depend on this target. + + This macro simplifies the process of setting up the DASH license checker by creating a reusable + `java_binary` target that adheres to the Bazel build rules and supports consistent license + validation across projects. + """ + dash_format_converter( + name = "formatted_deps", + requirement_file = src, + ) + + java_binary( + name = "license.check.{}".format(name), + main_class = "org.eclipse.dash.licenses.cli.Main", + runtime_deps = [ + "@dash_license_tool//jar", + ], + args = ["$(location :formatted_deps)"], + data = [ + ":formatted_deps", + ], + visibility = ["//visibility:public"], + ) diff --git a/tools/dash/formatters/BUILD b/tools/dash/formatters/BUILD new file mode 100644 index 00000000..3a54e0fe --- /dev/null +++ b/tools/dash/formatters/BUILD @@ -0,0 +1,20 @@ +# ******************************************************************************* +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +py_binary( + name = "dash_format_converter", + srcs = [ + "dash_format_converter.py", + ], + visibility = ["//visibility:public"], +) diff --git a/tools/dash/formatters/dash_format_converter.bzl b/tools/dash/formatters/dash_format_converter.bzl new file mode 100644 index 00000000..88c34ed5 --- /dev/null +++ b/tools/dash/formatters/dash_format_converter.bzl @@ -0,0 +1,51 @@ +# ******************************************************************************* +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +""" Bazel rule for generating dash formatted requirements file +""" + +def _impl(ctx): + """ The implementation function of the rule. + """ + + output = ctx.actions.declare_file("formatted.txt") + args = ctx.actions.args() + args.add("-i", ctx.file.requirement_file) + args.add("-o", output) + + ctx.actions.run( + inputs = [ctx.file.requirement_file], + outputs = [output], + arguments = [args], + progress_message = "Generating Dash formatted dependency file ...", + mnemonic = "DashFormat", + executable = ctx.executable._tool, + ) + return DefaultInfo(files = depset([output])) + +dash_format_converter = rule( + implementation = _impl, + attrs = { + "requirement_file": attr.label( + mandatory = True, + allow_single_file = True, + doc = "The requirement (requirement_lock.txt) input file which holds deps", + ), + "_tool": attr.label( + default = Label("//tools/dash/formatters:dash_format_converter"), + executable = True, + cfg = "exec", + doc = "", + ), + }, +) diff --git a/tools/dash/formatters/dash_format_converter.py b/tools/dash/formatters/dash_format_converter.py new file mode 100755 index 00000000..465c1691 --- /dev/null +++ b/tools/dash/formatters/dash_format_converter.py @@ -0,0 +1,239 @@ +# ******************************************************************************* +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +"""The tool for converting requirements.txt into dash checker format""" + +import re +import sys +import argparse +import logging +from pathlib import Path +from typing import Optional + +LOGGER = logging.getLogger() + +COLORS = { + "BLUE": "\033[34m", + "GREEN": "\033[32m", + "YELLOW": "\033[33m", + "RED": "\033[31m", + "DARK_RED": "\033[35;1m", + "ENDC": "\033[0m", +} + +LOGGER_COLORS = { + "DEBUG": COLORS["BLUE"], + "INFO": COLORS["GREEN"], + "WARNING": COLORS["YELLOW"], + "ERROR": COLORS["RED"], + "CRITICAL": COLORS["DARK_RED"], +} + + +class ColoredFormatter(logging.Formatter): + """ + A custom logging formatter to add color to log level names based on the logging level. + + The `ColoredFormatter` class extends `logging.Formatter` and overrides the `format` + method to add color codes to the log level name (e.g., `INFO`, `WARNING`, `ERROR`) + based on a predefined color mapping in `LOGGER_COLORS`. This color coding helps in + visually distinguishing log messages by severity. + + Attributes: + LOGGER_COLORS (dict): A dictionary mapping log level names (e.g., "INFO", "ERROR") + to their respective color codes. + COLORS (dict): A dictionary of terminal color codes, including an "ENDC" key to reset + colors after the level name. + + Methods: + format(record): Adds color to the `levelname` attribute of the log record and then + formats the record as per the superclass `Formatter`. + """ + + def format(self, record): + log_color = LOGGER_COLORS.get(record.levelname, "") + record.levelname = f"{log_color}{record.levelname}:{COLORS['ENDC']}" + return super().format(record) + + +def configure_logging(log_file_path: Path = None, verbose: bool = False) -> None: + """ + Configures the logging settings for the application. + + Args: + log_file_path (Path, optional): The file path where logs should be written. + If not provided, logs are written to the console. Defaults to `None`. + verbose (bool, optional): A flag that determines the logging level. + If `True`, sets the logging level to DEBUG; if `False`, sets it to INFO. + Defaults to `False`. + + Returns: + None: This function does not return any value, it only configures logging. + + Notes: + - If `log_file_path` is provided, the log messages will be saved to the specified file. + - If `verbose` is `True`, detailed logs (DEBUG level) will be captured; otherwise, + less detailed logs (INFO level) will be captured. + - The default logging format is `%(asctime)s - %(name)s - %(levelname)s - %(message)s`. + """ + log_level = logging.DEBUG if verbose else logging.INFO + LOGGER.setLevel(log_level) + LOGGER.handlers.clear() + + if log_file_path is not None: + handler = logging.FileHandler(log_file_path) + formatter = logging.Formatter("%(levelname)s: %(message)s") + else: + handler = logging.StreamHandler() + formatter = ColoredFormatter("%(levelname)s %(message)s") + + handler.setLevel(log_level) + handler.setFormatter(formatter) + LOGGER.addHandler(handler) + + +def format_line( + line: str, regex: str = r"([a-zA-Z0-9_-]+)==([a-zA-Z0-9.\-_]+)" +) -> Optional[str]: + """ + Formats a line of text by matching a specified regex pattern and extracting components. + + Args: + line (str): The input string to be processed. + regex (str, optional): A regular expression pattern to match the input line. + + Returns: + Optional[str]: A formatted string based on the regex match if the pattern is found, + or None if no match is found. + + Notes: + - The function uses the provided regex to capture two components from the input line: + the package name and its version. + - The formatted string follows the pattern "pypi/pypi/-/{package}/{version}". + - If the regex does not match, None is returned. + """ + + ret = None + match = re.match(f"{regex}", line) + + if match: + package, version = match.groups() + ret = f"pypi/pypi/-/{package}/{version}" + + return ret + + +def convert_to_dash_format(input_file: Path, output_file: Path) -> int: + """ + Converts the content of an input to a "dash format" and writes output file. + + The exact transformation applied to the content is assumed to replace specific patterns or + structures with a dash-separated format, although the details depend on the implementation. + + Args: + input_file (Path): Path to the input file containing the original content. + output_file (Path): Path to the output file where the converted content will be written. + + Returns: + int: Error thrown by system over exceptions. + """ + encoding = "utf-8" + with open(input_file, "r", encoding=encoding) as infile: + with open(output_file, "w", encoding=encoding) as outfile: + for line in infile: + formatted_line = format_line(line.strip()) + if formatted_line: + outfile.write(formatted_line + "\n") + + +def parse_arguments(argv: list[str]) -> argparse.Namespace: + """ + Parses command-line arguments passed to the script. + + Args: + argv (list[str]): A list of command-line arguments, typically `sys.argv[1:]`. + + Returns: + argparse.Namespace: An object containing the parsed arguments as attributes. + + Notes: + - This function expects an `argparse.ArgumentParser` to be configured with + the required arguments. If `argv` is not provided, it defaults to an empty list. + - Use the `argparse.Namespace` object to access parsed arguments by their names. + """ + + parser = argparse.ArgumentParser( + description="The tool for converting requirements.txt into dash \ + checker format." + ) + + parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="Enable debug logging level", + ) + + parser.add_argument( + "-l", + "--log-file", + type=Path, + default=None, + help="Redirect logs from STDOUT to this file", + ) + + parser.add_argument( + "-i", + "--input", + type=Path, + required=True, + help="Path to the requirement.txt file", + ) + + parser.add_argument( + "-o", + "--output", + type=Path, + required=True, + help="Path to the formatted_list.txt file", + ) + + return parser.parse_args(argv) + + +def main(argv: list[str] = None) -> int: + """ + The main entry point of the script. + + Args: + argv (list[str], optional): A list of command-line arguments. If not provided, + defaults to `None`, in which case `sys.argv[1:]` is typically used. + + Returns: + int: An exit code where `0` indicates successful execution, and any non-zero + value indicates an error. + + Notes: + - This function is often called in the `if __name__ == "__main__":` block. + - The function typically orchestrates parsing arguments, performing the core + logic of the script, and handling exceptions. + - Ensure the function catches and logs errors appropriately before returning + a non-zero exit code. + """ + args = parse_arguments(argv if argv is not None else sys.argv[1:]) + configure_logging(args.log_file, args.verbose) + convert_to_dash_format(args.input, args.output) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:]))