From 95d3df73fe975a54a4cd38a0aff02f1a6fb37732 Mon Sep 17 00:00:00 2001 From: MaximilianSoerenPollak Date: Wed, 15 Jul 2026 11:50:07 +0200 Subject: [PATCH 01/22] Feat: Add cr_checker --- .bazelrc | 41 ++ .bazelversion | 1 + .github/workflows/pre-commit.yml | 27 + .github/workflows/tests.yml | 39 ++ .gitignore | 11 + .pre-commit-config.yaml | 22 + .pre-commit-hooks.yaml | 20 + LICENSE | 177 +++++ LICENSES/Apache-2.0.txt | 73 ++ LICENSES/EPL-2.0.txt | 80 +++ MODULE.bazel | 22 + NOTICE | 38 ++ README.md | 62 +- REUSE.toml | 49 ++ cr_checker/BUILD | 0 cr_checker/LICENSE | 13 + cr_checker/README.md | 199 ++++++ cr_checker/cr_checker.bzl | 135 ++++ cr_checker/deps.bzl | 37 + cr_checker/resources/BUILD | 36 + cr_checker/resources/config.json | 3 + cr_checker/resources/exclusion.txt | 1 + cr_checker/resources/templates.ini | 91 +++ cr_checker/tests/.bazelrc | 6 + cr_checker/tests/.bazelversion | 1 + cr_checker/tests/BUILD | 24 + cr_checker/tests/MODULE.bazel | 48 ++ cr_checker/tests/requirements_lock.txt | 1 + cr_checker/tests/test_cr_checker.py | 461 +++++++++++++ cr_checker/tool/BUILD | 21 + cr_checker/tool/cr_checker.py | 904 +++++++++++++++++++++++++ cr_checker/tool/pre-commit_wrapper | 40 ++ 32 files changed, 2681 insertions(+), 2 deletions(-) create mode 100644 .bazelrc create mode 100644 .bazelversion create mode 100644 .github/workflows/pre-commit.yml create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 .pre-commit-hooks.yaml create mode 100644 LICENSE create mode 100644 LICENSES/Apache-2.0.txt create mode 100644 LICENSES/EPL-2.0.txt create mode 100644 MODULE.bazel create mode 100644 NOTICE create mode 100644 REUSE.toml create mode 100644 cr_checker/BUILD create mode 100644 cr_checker/LICENSE create mode 100644 cr_checker/README.md create mode 100644 cr_checker/cr_checker.bzl create mode 100644 cr_checker/deps.bzl create mode 100644 cr_checker/resources/BUILD create mode 100644 cr_checker/resources/config.json create mode 100644 cr_checker/resources/exclusion.txt create mode 100644 cr_checker/resources/templates.ini create mode 100644 cr_checker/tests/.bazelrc create mode 120000 cr_checker/tests/.bazelversion create mode 100644 cr_checker/tests/BUILD create mode 100644 cr_checker/tests/MODULE.bazel create mode 100644 cr_checker/tests/requirements_lock.txt create mode 100644 cr_checker/tests/test_cr_checker.py create mode 100644 cr_checker/tool/BUILD create mode 100755 cr_checker/tool/cr_checker.py create mode 100755 cr_checker/tool/pre-commit_wrapper diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 0000000..2d44ee2 --- /dev/null +++ b/.bazelrc @@ -0,0 +1,41 @@ +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# SPDX-License-Identifier: Apache-2.0 + +common --registry=https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/ +common --registry=https://bcr.bazel.build + +#common --extra_toolchains=@score_gcc_x86_64_toolchain//:x86_64-linux + +# libclang toolchain for the C/C++ parser used by the score rules. Registered +# only for score_tooling's own build (its LLVM is a dev dependency); integrating +# repositories register their own libclang toolchain instead. +#common --extra_toolchains=//cpp/libclang:score_tooling_libclang_toolchain + +build --java_language_version=17 +build --tool_java_language_version=17 +build --java_runtime_version=remotejdk_17 +build --tool_java_runtime_version=remotejdk_17 + +# Use GNU ld (bfd) as the Rust linker; avoids the deprecation warning that +# newer Rust emits when it detects the system default linker is gold. +#build --@rules_rust//rust/settings:extra_rustc_flag=-Clink-arg=-fuse-ld=bfd + +# Rust clippy linter +#build:clippy --aspects=@rules_rust//rust:defs.bzl%rust_clippy_aspect +#build:clippy --output_groups=+clippy_checks + +# Log level configuration for rules_score build output +# normal build: only errors and warnings (default – no flag needed) +# info build: additionally show info messages from all tools +build:info --//bazel/rules/rules_score:verbosity=info +# debug build: complete output including debug/trace from all tools +build:debug --//bazel/rules/rules_score:verbosity=debug + +# Standard combined coverage (Rust + Python, no Ferrocene required) +# Usage: bazel coverage --config=coverage +# Then run: bazel run //coverage:combined_report +coverage:coverage --combined_report=lcov +#coverage:coverage --@rules_rust//rust/settings:extra_rustc_flag=-Clink-dead-code +#coverage:coverage --@rules_rust//rust/settings:extra_rustc_flag=-Ccodegen-units=1 + diff --git a/.bazelversion b/.bazelversion new file mode 100644 index 0000000..acd405b --- /dev/null +++ b/.bazelversion @@ -0,0 +1 @@ +8.6.0 diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..1b398af --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,27 @@ +# ******************************************************************************* +# Copyright (c) 2026 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: pre-commit +on: + pull_request: + types: [opened, reopened, synchronize] +jobs: + self_test: + name: 🔬 Self Test + runs-on: ubuntu-latest + steps: + - name: 📥 Check out + uses: actions/checkout@v7.0.0 + - name: ⚙️ Setup uv + uses: astral-sh/setup-uv@v7 + - name: 🛠️ Run pre-commit + run: uvx pre-commit run --all-files diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..44232e6 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,39 @@ +# ******************************************************************************* +# Copyright (c) 2026 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 +# ******************************************************************************* + integration_tests: + runs-on: ubuntu-24.04 + permissions: + contents: read + actions: write + steps: + - name: Checkout repository + uses: actions/checkout@v7.0.0 + - name: Free Disk Space (Ubuntu) + uses: eclipse-score/more-disk-space@v1 + with: + level: 4 + - uses: castler/setup-bazel@cache-optimized + with: + bazelisk-cache: true + disk-cache: integration_tests + repository-cache: true + cache-optimized: true + cache-save: ${{ github.ref == 'refs/heads/main' }} + # - name: Run python_basics integration tests + # run: | + # cd python_basics/integration_tests + # bazel test //... + - name: Run cr_checker unit tests + run: | + cd cr_checker/tests + bazel test //... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9c1191b --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# SPDX-License-Identifier: Apache-2.0 + +bazel-* +MODULE.bazel.lock +external +.vscode/ + +__pycache__ +.ruff_cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..d2a5d82 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,22 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # v6.0.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + exclude: requirements_lock.txt|\.svg$ + - id: trailing-whitespace + - id: check-shebang-scripts-are-executable + - id: check-executables-have-shebangs + - id: check-added-large-files + args: [--maxkb=100, --enforce-all] # increase or add git lfs if too strict + exclude: org.eclipse.dash.licenses-1.1.0.jar|blanket_index.html + - repo: https://github.com/google/yamlfmt + rev: 21ca5323a9c87ee37a434e0ca908efc0a89daa07 # v0.21.0 + hooks: + - id: yamlfmt + - repo: https://codeberg.org/fsfe/reuse-tool + rev: a1bb792acda6fd0724936b4ebbdbc8eceb9c0459 # v6.2.0 + hooks: + - id: reuse-lint-file + exclude: \.(svg|tpl|txt|conf|lock|json|in|png)$ diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 0000000..3607a34 --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,20 @@ +# ******************************************************************************* +# Copyright (c) 2026 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 +# ******************************************************************************* +- id: copyright + name: Check and fix copyright headers with cr_checker + entry: cr_checker/tool/pre-commit_wrapper --extensions h hpp c cpp rs rst py sh bzl ini yml yaml trlc rsl puml svg BUILD bazel --fix + # this would be the better language implementation, but struggle with python tooling + # language: python + language: script + minimum_pre_commit_version: 3.2.0 + types_or: [python, yaml, ini, rst, sh, shell, bash, bazel, c, c++, rust, plantuml, svg] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f433b1a --- /dev/null +++ b/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt new file mode 100644 index 0000000..137069b --- /dev/null +++ b/LICENSES/Apache-2.0.txt @@ -0,0 +1,73 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed 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. diff --git a/LICENSES/EPL-2.0.txt b/LICENSES/EPL-2.0.txt new file mode 100644 index 0000000..78756b5 --- /dev/null +++ b/LICENSES/EPL-2.0.txt @@ -0,0 +1,80 @@ +Eclipse Public License - v 2.0 +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE (“AGREEMENT”). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS +“Contribution” means: + +a) in the case of the initial Contributor, the initial content Distributed under this Agreement, and +b) in the case of each subsequent Contributor: +i) changes to the Program, and +ii) additions to the Program; +where such changes and/or additions to the Program originate from and are Distributed by that particular Contributor. A Contribution “originates” from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include changes or additions to the Program that are not Modified Works. +“Contributor” means any person or entity that Distributes the Program. + +“Licensed Patents” mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. + +“Program” means the Contributions Distributed in accordance with this Agreement. + +“Recipient” means anyone who receives the Program under this Agreement or any Secondary License (as applicable), including Contributors. + +“Derivative Works” shall mean any work, whether in Source Code or other form, that is based on (or derived from) the Program and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. + +“Modified Works” shall mean any work in Source Code or other form that results from an addition to, deletion from, or modification of the contents of the Program, including, for purposes of clarity any new file in Source Code form that contains any contents of the Program. Modified Works shall not include works that contain only declarations, interfaces, types, classes, structures, or files of the Program solely in each case in order to link to, bind by name, or subclass the Program or Modified Works thereof. + +“Distribute” means the acts of a) distributing or b) making available in any manner that enables the transfer of a copy. + +“Source Code” means the form of a Program preferred for making modifications, including but not limited to software source code, documentation source, and configuration files. + +“Secondary License” means either the GNU General Public License, Version 2.0, or any later versions of that license, including any exceptions or additional permissions as identified by the initial Contributor. + +2. GRANT OF RIGHTS +a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, Distribute and sublicense the Contribution of such Contributor, if any, and such Derivative Works. +b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in Source Code or other form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. +c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to Distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. +d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. +e) Notwithstanding the terms of any Secondary License, no Contributor makes additional grants to any Recipient (other than those set forth in this Agreement) as a result of such Recipient's receipt of the Program under the terms of a Secondary License (if permitted under the terms of Section 3). +3. REQUIREMENTS +3.1 If a Contributor Distributes the Program in any form, then: + +a) the Program must also be made available as Source Code, in accordance with section 3.2, and the Contributor must accompany the Program with a statement that the Source Code for the Program is available under this Agreement, and informs Recipients how to obtain it in a reasonable manner on or through a medium customarily used for software exchange; and +b) the Contributor may Distribute the Program under a license different than this Agreement, provided that such license: +i) effectively disclaims on behalf of all other Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; +ii) effectively excludes on behalf of all other Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; +iii) does not attempt to limit or alter the recipients' rights in the Source Code under section 3.2; and +iv) requires any subsequent distribution of the Program by any party to be under a license that satisfies the requirements of this section 3. +3.2 When the Program is Distributed as Source Code: + +a) it must be made available under this Agreement, or if the Program (i) is combined with other material in a separate file or files made available under a Secondary License, and (ii) the initial Contributor attached to the Source Code the notice described in Exhibit A of this Agreement, then the Program may be made available under the terms of such Secondary Licenses, and +b) a copy of this Agreement must be included with each copy of the Program. +3.3 Contributors may not remove or alter any copyright, patent, trademark, attribution notices, disclaimers of warranty, or limitations of liability (‘notices’) contained within the Program from any copy of the Program which they Distribute, provided that Contributors may add their own appropriate notices. + +4. COMMERCIAL DISTRIBUTION +Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor (“Commercial Contributor”) hereby agrees to defend and indemnify every other Contributor (“Indemnified Contributor”) against any losses, damages and costs (collectively “Losses”) arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. + +5. NO WARRANTY +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement, including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL +If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be Distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to Distribute the Program (including its Contributions) under the new version. + +Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. Nothing in this Agreement is intended to be enforceable by any entity that is not a Contributor or Recipient. No third-party beneficiary rights are created under this Agreement. + +Exhibit A – Form of Secondary Licenses Notice +“This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), version(s), and exceptions or additional permissions here}.” + +Simply including a copy of this Agreement, including this Exhibit A is not sufficient to license the Source Code under Secondary Licenses. + +If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + +You may add additional accurate notices of copyright ownership. diff --git a/MODULE.bazel b/MODULE.bazel new file mode 100644 index 0000000..5c512d5 --- /dev/null +++ b/MODULE.bazel @@ -0,0 +1,22 @@ +# ******************************************************************************* +# Copyright (c) 2026 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 +# ******************************************************************************* + +module( + name = "score_utils" +) +# CR_CHECKER +bazel_dep(name = "aspect_rules_py", version = "1.4.0") +bazel_dep(name = "score_tooling", version = "1.2.0") +# bazel_dep(name = "rules_python", version = "1.4.1") +# bazel_dep(name = "rules_rust", version = "0.68.1-score") +# bazel_dep(name = "rules_shell", version = "0.5.0") diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..9c933ea --- /dev/null +++ b/NOTICE @@ -0,0 +1,38 @@ + + +# Notices for Eclipse Safe Open Vehicle Core + +This content is produced and maintained by the Eclipse Safe Open Vehicle Core project. + + * Project home: https://projects.eclipse.org/projects/automotive.score + +## Trademarks + +Eclipse, and the Eclipse Logo are registered trademarks of the Eclipse Foundation. + +## Copyright + +All content is the property of the respective authors or their employers. +For more information regarding authorship of content, please consult the +listed source code repository logs. + +## Declared Project Licenses + +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 + +## Cryptography + +Content may contain encryption software. The country in which you are currently +may have restrictions on the import, possession, and use, and/or re-export to +another country, of encryption software. BEFORE using any encryption software, +please check the country's laws, regulations and policies concerning the import, +possession, or use, and re-export of encryption software, to see if this is +permitted. diff --git a/README.md b/README.md index fbff2dd..5a59127 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,60 @@ -# tools -Home of score-tools, the new pypi based tools approach + + +# Tools + +## Why this repository exists + +The utilities/tools in this repository used to live inside [tooling](https://github.com/eclipse-score/tooling). That +repo works well for its main purpose, but it also pulls in a large and +growing dependency graph because it bundles many unrelated concerns together. + +That created a real cost for consumers: if you only needed one small, +self-contained utility, building or depending on it meant pulling in the +entire dependency graph of `tooling`, even though 99% of it was +irrelevant to what you actually wanted. + +This repository exists to fix that. It pulls a set of small, self-contained +utilities out of `tooling` into their own home, so that depending on +a utility costs you only what that utility actually needs — not the +dependency footprint of an unrelated, much larger codebase. + +## Scope + +This repo is intentionally narrow. To stay useful, it needs to *stay* +narrow — the whole point of splitting it out was to avoid recreating the +same problem in a new location. + +**In scope:** +- Small, self-contained, general-purpose utilities with minimal external + dependencies. +- Code with no coupling to `tooling`. +- Utilities that are (or are likely to be) reused across multiple, + otherwise-unrelated projects. + +**Out of scope:** +- Anything tied to a specific product, service, or domain — that belongs in + the repo that owns that domain. +- Utilities that only make sense in the context of `tooling`. +- Anything that would pull in a large or heavyweight dependency for the + benefit of a single utility. If a proposed addition needs a big + dependency, prefer keeping it in whichever repo already depends on that + library, or give it its own repo instead of adding it here. +- Grab-bag/"misc" code with no clear justification for living here. When in + doubt, ask whether this utility would want to be depended on by something + that doesn't want any of the other dependencies this repo would then + bring in. If the answer isn't clearly yes, it doesn't belong here. + +## Adding something new + +Before adding a new utility here, check: +1. Is it genuinely general-purpose, not tied to one product's domain logic? +2. Does it avoid pulling in dependencies that most other things in this + repo don't already need? +3. Would splitting it into its own small repo/target actually serve + consumers better than adding it here? + +If any of these gives you pause, raise it for discussion before merging. diff --git a/REUSE.toml b/REUSE.toml new file mode 100644 index 0000000..6c94d54 --- /dev/null +++ b/REUSE.toml @@ -0,0 +1,49 @@ +# ******************************************************************************* +# Copyright (c) 2026 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 +# ******************************************************************************* + +version = 1 + +[[annotations]] +path = [".devcontainer/*.json"] +SPDX-FileCopyrightText = "Copyright (c) 2026 Contributors to the Eclipse Foundation" +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = [".vscode/*.json"] +SPDX-FileCopyrightText = "Copyright (c) 2026 Contributors to the Eclipse Foundation" +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = ["**/*.lock.json"] +SPDX-FileCopyrightText = "Copyright (c) 2026 Contributors to the Eclipse Foundation" +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = ["**/requirements_lock.txt"] +SPDX-FileCopyrightText = "Copyright (c) 2026 Contributors to the Eclipse Foundation" +SPDX-License-Identifier = "Apache-2.0" + + +[[annotations]] +path = ["cr_checker/resources/config.json", + "**/.bazelversion", + "coverage/tests/fixtures/symbol_report.json", + "python_basics/.bazelversion", + "python_basics/integration_tests/venv-with-extra-requirements/requirements.in", + "python_basics/requirements.in", + "python_basics/requirements.txt", + "bazel/rules/rules_score/test/requirements.in", + "bazel/rules/rules_score/test/requirements.txt" + ] +SPDX-FileCopyrightText = "Copyright (c) 2026 Contributors to the Eclipse Foundation" +SPDX-License-Identifier = "Apache-2.0" diff --git a/cr_checker/BUILD b/cr_checker/BUILD new file mode 100644 index 0000000..e69de29 diff --git a/cr_checker/LICENSE b/cr_checker/LICENSE new file mode 100644 index 0000000..8c69a8b --- /dev/null +++ b/cr_checker/LICENSE @@ -0,0 +1,13 @@ +Copyright 2025 Contributors to the Eclipse Foundation + +Licensed 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. diff --git a/cr_checker/README.md b/cr_checker/README.md new file mode 100644 index 0000000..e9d9833 --- /dev/null +++ b/cr_checker/README.md @@ -0,0 +1,199 @@ + + +# CopyRight Checker + +`cr_checker.py` is a tool designed to check if files contain a specified copyright header. It provides configurable logging, color-coded console output, and can handle large file sets efficiently. The script supports reading configuration files for custom copyright templates and can utilize memory-mapped file reading for better performance with large files. Tool itself can also append copyright header at the beginning of file if flag `--fix` is used. + +## Features + +- Checks files for specified copyright headers based on file extensions. +- Configurable logging, including color-coded output for easy visibility of log levels. +- Supports parameter files for flexible input handling. +- Can use memory mapping for large file handling. +- Customizable file encoding and offset adjustments for header text positioning. +- Can append copyright headers. +- Can remove provided number of characters from beginning of the file. + +## Requirements + +- Python 3.6+ +- `argparse`, `logging`, `os`, `sys`, `mmap`, `tempfile`, and `pathlib` (standard library modules) + +## Installation + +To use `cr_checker.py`, simply clone this repository. + +## Usage + +The script can be run from the command line with various options to customize its behavior: + +```bash +python cr_checker.py -t [options] +``` + +### Arguments + +- **-t**, **--template-file**: (Required) Path to the template file that defines the copyright text for each file extension. +- **-v**, **--verbose**: Enable debug-level logging. +- **-l**, **--log-file**: Path to a log file where logs will be saved. If not provided, logs will print to the console. +- **-e**, **--extensions**: List of file extensions to filter, e.g., -e .py .cpp. +- **--use_memory_map**: Use memory-mapped file reading for large files. +- **--encoding**: File encoding (default is utf-8). +- **--offset**: Additional offset for the header length to account for lines like a shebang. +- **--fix**: Setting script into fix mode where copyright header will be added to the files if it's missing from same. +- **--remove-offset**: Number of characters to remove before appending proper copyright header (works only with `--fix` option). +- **inputs**: (Required) Directories or files to parse, or a parameter file prefixed with @ that lists files or directories. + +> NOTE: Option `--remove-offset` can have severe consequences if the offset is miscalculated. Use with **extreme caution**. + +> NOTE: Setting directory as `.` will cause that tool removes your complete workspace! This is connected with how Bazel includes python into build. **DO NOT USE THIS OPTION UNLESS YOU'RE 100% SURE IN WHAT YOU'RE DOING**. + +### Examples + +```sh +python cr_checker.py -t templates.ini -e py cpp -v -l logs.txt my_random_file.cpp my_random_file.py + +python cr_checker.py -t templates.ini -e py cpp --offset 24 --use_memory_map @files_to_check.txt + +python cr_checker.py -t templates.ini -e py cpp --fix --offset 24 --use_memory_map @files_to_check.txt + +``` + +#### A bit more about `--offset` + +This mode is special that will enable tool to do advance search & replace copyright headers. For example, assuming that we have following python implementation: + +```python +#!/usr/bin/env python3 + +import os +``` + +and we use following command: + +```sh +python cr_checker.py -t templates.ini -e py cpp --fix --use_memory_map @files_to_check.txt +``` + +The result will be following: + +```python +################## +# COPYRIGHT HEADER +################## +#!/usr/bin/env python3 + +import os +``` + +This is of course not what we want, and for that we use `--offset=24` where 24 is number of char + 1 (for new line char). +if we apply this arguments now on same file, the outcome is different. + +```python +#!/usr/bin/env python3 + +################## +# COPYRIGHT HEADER +################## + +import os +``` + +On another hand, `--offset` has also another role. Assuming that a header text in file is soo big that copyright header is in the middle of the file, with regular command, the tool will not detect copyright headed. With `--offset=` we can tell the tool from where to start considering the search for +copyright header. + +### Template File Format + +The template file should be in INI format, with each section representing a file extension and a section specifying the copyright text. +The copyright text can use format expressions to match the year and the author. + +Example templates.ini: + +```ini +[py,sh] +# Copyright (c) {year} {author} + +[cpp,c,hpp, h] +// Copyright (c) {year} {author} +``` + +## Exit Codes + +- 0: All files contain the required copyright text. +- 1: Some files are missing the required copyright text. +- Other: Error encountered during file processing. + +### Logging and Color-Coded Output + +By default, logs are printed to the console in color-coded format to indicate log levels. You can redirect logs to a file using the -l option. + +#### Log Colors + +- DEBUG: Blue +- INFO: Green +- WARNING: Yellow +- ERROR: Red + +## Bazel integration + +### Copyright Checker Bazel Macro + +To integrate copyright verification into your Bazel-based project, you can use the `copyright_checker` macro. This macro allows you to check source files for compliance with a specified copyright template and configuration. Additionally, it can automatically apply fixes when necessary. + +#### Usage + +```python +load("@score_cr_checker//cr_checker:def.bzl", "copyright_checker") +copyright_checker( + name = "copyright_check", + srcs = glob(["src/**/*.cpp", "src/**/*.h"]), + config = "@score_cr_checker//tools/cr_checker/resources:config", + template = "@score_cr_checker//tools/cr_checker/resources:templates", + visibility = ["//visibility:public"], +) +``` + +#### Parameters + +- **name**: Unique identifier for the rule. +- **srcs**: List of source files to check. +- **visibility**: Defines which targets can access this rule. +- **template**: Path to the copyright header template. +- **config**: Path to the project-specific configuration. +- **extensions** (optional): List of file extensions to filter files. Defaults to all files. +- **offset** (optional): Line offset for checking/modifying files. +- **remove_offset** (optional): Number of characters to remove from the beginning of the file. +- **debug** (optional): Enables verbose logging for debugging. +- **use_memory_map** (optional): Uses memory-mapped files for performance optimization. +- **fix** (optional): Automatically applies fixes instead of just reporting issues. + +### Integrate `cr_checker` using Bazel module + +The CopyRight check tool is integrated in other bazel repository using Bazel modules mechanism. The current tool is not registered within BCR so private Bazel registry needs to be select. To select custom Bazel registry add following lines into .bazelrc: + +```python +common --registry=https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/ +common --registry=https://bcr.bazel.build +``` + +This will allow Bazel to look into project Bazel registry. After that all what is needed is to add following lines in MODULE.bazel: + +```python +############################################################################### +# +# CopyRight checker dependencies +# +############################################################################### +bazel_dep(name = "score_cr_checker", version = "0.2.2") +``` diff --git a/cr_checker/cr_checker.bzl b/cr_checker/cr_checker.bzl new file mode 100644 index 0000000..c567554 --- /dev/null +++ b/cr_checker/cr_checker.bzl @@ -0,0 +1,135 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +"""Defines Bazel rules for running copyright checks and fixes.""" + +load("@aspect_rules_py//py:defs.bzl", "py_binary") + +def copyright_checker( + name, + srcs, + visibility, + template, + config, + exclusion = None, + extensions = [], + offset = 0, + remove_offset = 0, + debug = False, + use_memory_map = False, + fix = False, + target_compatible_with = None): + """ + Defines a custom build rule for checking and optionally fixing files for compliance + with specific requirements, such as copyright headers. + + Args: + name (str): The name of the rule, used as an identifier in the build system. + srcs (list): A list of source file paths to check. + visibility (list): A list defining the visibility of the rule, specifying which + targets can use this rule. + template (str, optional): Path to the template resource used for validation. + Defaults to "//tools/cr_checker/resources:templates". + config (str, optional): Path to the config resource used for project variables. + Defaults to "//tools/cr_checker/resources:config". + exclusion (str, optional): Path to a text file listing files to be excluded from the copyright check. + File format: one path per line, relative to the repository root. + extensions (list, optional): A list of file extensions to filter the source files. + Defaults to an empty list, meaning all files are checked. + offset (int, optional): The line offset for applying checks or modifications. + Defaults to 0. + remove_offset (int, optional): The line offset for removing chars from beginning of file. + Defaults to 0. + debug (bool, optional): Whether to enable debug mode, providing additional logs. + Defaults to False. + use_memory_map (bool, optional): Whether to use memory mapping for large files to + improve performance. Defaults to False. + fix (bool, optional): Whether to apply fixes to files instead of just reporting issues. + Defaults to False. + + Returns: + None: This function defines a rule for a build system and does not return a value. + """ + t_names = [ + "{}.check".format(name), + "{}.fix".format(name), + ] + + args = [ + "-t $(location {})".format(template), + "-c $(location {})".format(config), + ] + if len(extensions): + args.append("-e {exts}".format( + exts = " ".join([exts for exts in extensions]), + )) + + if exclusion: + args.append("--exclusion-file $(location {})".format(exclusion)) + + if offset: + args.append("--offset {}".format(offset)) + + if debug: + args.append("-v") + + if use_memory_map: + args.append("--use_memory_map") + + for src in srcs: + args.append("$(locations {})".format(src)) + + for t_name in t_names: + if t_name == "{}.fix".format(name): + args.insert(0, "--fix") + if remove_offset: + args.append("--remove_offset {}".format(remove_offset)) + + data = srcs[:] + data.append(template) + data.append(config) + if exclusion: + data.append(exclusion) + + py_binary( + name = t_name, + main = "cr_checker.py", + srcs = [ + "@score_tooling//cr_checker/tool:cr_checker_lib", + ], + args = args, + data = data, + visibility = visibility, + target_compatible_with = target_compatible_with, + ) + + native.alias( + name = "copyright-check", + actual = ":" + name + ".check", + visibility = visibility, + target_compatible_with = target_compatible_with, + tags = [ + "cli_help=Check for license headers:\n" + + "bazel run //:copyright-check", + ], + ) + + native.alias( + name = "copyright-fix", + actual = ":" + name + ".fix", + visibility = visibility, + tags = [ + "cli_help=Fix license headers:\n" + + "bazel run //:copyright-fix", + ], + ) diff --git a/cr_checker/deps.bzl b/cr_checker/deps.bzl new file mode 100644 index 0000000..bf84784 --- /dev/null +++ b/cr_checker/deps.bzl @@ -0,0 +1,37 @@ +# ******************************************************************************* +# 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("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +def score_cr_checker_deps(): + if not native.existing_rule("rules_cc"): + http_archive( + name = "rules_cc", + urls = ["https://github.com/bazelbuild/rules_cc/releases/download/0.1.1/rules_cc-0.1.1.tar.gz"], + sha256 = "712d77868b3152dd618c4d64faaddefcc5965f90f5de6e6dd1d5ddcd0be82d42", + strip_prefix = "rules_cc-0.1.1", + ) + if not native.existing_rule("aspect_rules_py"): + http_archive( + name = "rules_python", + sha256 = "9f9f3b300a9264e4c77999312ce663be5dee9a56e361a1f6fe7ec60e1beef9a3", + strip_prefix = "rules_python-1.4.1", + url = "https://github.com/bazel-contrib/rules_python/releases/download/1.4.1/rules_python-1.4.1.tar.gz", + ) + if not native.existing_rule("aspect_rules_py"): + http_archive( + name = "aspect_rules_py", + sha256 = "8e9a1f00e4ba5696f9e93a770a6c1de863544cce489df91809fc3a4027ccfddc", + strip_prefix = "rules_py-1.4.0", + url = "https://github.com/aspect-build/rules_py/releases/download/v1.4.0/rules_py-v1.4.0.tar.gz", + ) diff --git a/cr_checker/resources/BUILD b/cr_checker/resources/BUILD new file mode 100644 index 0000000..307734e --- /dev/null +++ b/cr_checker/resources/BUILD @@ -0,0 +1,36 @@ +# ******************************************************************************* +# Copyright (c) 2025 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 +# ******************************************************************************* + +filegroup( + name = "templates", + srcs = [ + "templates.ini", + ], + visibility = ["//visibility:public"], +) + +filegroup( + name = "config", + srcs = [ + "config.json", + ], + visibility = ["//visibility:public"], +) + +filegroup( + name = "exclusion", + srcs = [ + "exclusion.txt", + ], + visibility = ["//visibility:public"], +) diff --git a/cr_checker/resources/config.json b/cr_checker/resources/config.json new file mode 100644 index 0000000..285c132 --- /dev/null +++ b/cr_checker/resources/config.json @@ -0,0 +1,3 @@ +{ + "author": "Contributors to the Eclipse Foundation" +} diff --git a/cr_checker/resources/exclusion.txt b/cr_checker/resources/exclusion.txt new file mode 100644 index 0000000..73d6ffc --- /dev/null +++ b/cr_checker/resources/exclusion.txt @@ -0,0 +1 @@ +cr_checker/resources/templates.ini diff --git a/cr_checker/resources/templates.ini b/cr_checker/resources/templates.ini new file mode 100644 index 0000000..d9f1220 --- /dev/null +++ b/cr_checker/resources/templates.ini @@ -0,0 +1,91 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +[cpp,c,h,hpp,trlc,rsl] +/******************************************************************************** + * Copyright (c) {year} {author} + * + * 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,sh,bzl,ini,yml,yaml,BUILD,bazel] +# ******************************************************************************* +# Copyright (c) {year} {author} +# +# 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 +# ******************************************************************************* +[rst] +.. + # ******************************************************************************* + # Copyright (c) {year} {author} + # + # 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 + # ******************************************************************************* +[rs] +// ******************************************************************************* +// Copyright (c) {year} {author} +// +// 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +[puml] +' ******************************************************************************* +' Copyright (c) {year} 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 +' ******************************************************************************* +[md] + diff --git a/cr_checker/tests/.bazelrc b/cr_checker/tests/.bazelrc new file mode 100644 index 0000000..b425658 --- /dev/null +++ b/cr_checker/tests/.bazelrc @@ -0,0 +1,6 @@ +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# SPDX-License-Identifier: Apache-2.0 + +common --registry=https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/ +common --registry=https://bcr.bazel.build diff --git a/cr_checker/tests/.bazelversion b/cr_checker/tests/.bazelversion new file mode 120000 index 0000000..96cf949 --- /dev/null +++ b/cr_checker/tests/.bazelversion @@ -0,0 +1 @@ +../../.bazelversion \ No newline at end of file diff --git a/cr_checker/tests/BUILD b/cr_checker/tests/BUILD new file mode 100644 index 0000000..3bc90fc --- /dev/null +++ b/cr_checker/tests/BUILD @@ -0,0 +1,24 @@ +# ******************************************************************************* +# Copyright (c) 2025 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("@score_tooling//python_basics:defs.bzl", "score_py_pytest") + +score_py_pytest( + name = "unit_tests", + srcs = [ + "test_cr_checker.py", + ], + deps = [ + "@score_tooling//cr_checker/tool:cr_checker_lib", + ], +) diff --git a/cr_checker/tests/MODULE.bazel b/cr_checker/tests/MODULE.bazel new file mode 100644 index 0000000..57b285e --- /dev/null +++ b/cr_checker/tests/MODULE.bazel @@ -0,0 +1,48 @@ +# ******************************************************************************* +# Copyright (c) 2025 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 +# ******************************************************************************* +module( + name = "score_cr_checker_tests", + version = "0.1.0", +) + +# PYTHON +bazel_dep(name = "rules_python", version = "1.4.1") + +PYTHON_VERSION = "3.12" + +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.toolchain( + python_version = PYTHON_VERSION, +) +use_repo(python) + +# PIP +pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") +pip.parse( + hub_name = "pip_deps_test", + python_version = PYTHON_VERSION, + requirements_lock = "//:requirements_lock.txt", +) +use_repo(pip, "pip_deps_test") + +bazel_dep(name = "score_utils", version = "0.0.0") +local_path_override( + module_name = "score_utils", + path = "../../", +) +bazel_dep(name = "score_tooling", version = "1.2.0") + +bazel_dep(name = "rules_rust", version = "0.68.1-score") +bazel_dep(name = "rules_shell", version = "0.5.0") + +# end Tests diff --git a/cr_checker/tests/requirements_lock.txt b/cr_checker/tests/requirements_lock.txt new file mode 100644 index 0000000..e504a61 --- /dev/null +++ b/cr_checker/tests/requirements_lock.txt @@ -0,0 +1 @@ +bazel-runfiles==1.3.0 \ No newline at end of file diff --git a/cr_checker/tests/test_cr_checker.py b/cr_checker/tests/test_cr_checker.py new file mode 100644 index 0000000..48bc842 --- /dev/null +++ b/cr_checker/tests/test_cr_checker.py @@ -0,0 +1,461 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* + +# unit tests for the shebang handling in the cr_checker module +from __future__ import annotations + +import importlib.util +import json +import pytest +from datetime import datetime +from pathlib import Path + + +# load the cr_checker module +def load_cr_checker_module(): + module_path = Path(__file__).resolve().parents[1] / "tool" / "cr_checker.py" + spec = importlib.util.spec_from_file_location("cr_checker_module", module_path) + if spec is None or spec.loader is None: + raise RuntimeError(f"Failed to load cr_checker module from {module_path}") + + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +# load the license template +def load_template(extension: str) -> str: + cr_checker = load_cr_checker_module() + template_file = Path(__file__).resolve().parents[1] / "resources" / "templates.ini" + templates = cr_checker.load_templates(template_file) + return templates[extension] + + +# write the config file here so that the year is always up to date with the year +# written in the test file +def write_config(path: Path, author: str) -> Path: + config_path = path / "config.json" + config_path.write_text(json.dumps({"author": author}), encoding="utf-8") + return config_path + + +# test that offset matches the length of the shebang line including trailing newlines +def test_detect_shebang_offset_counts_trailing_newlines(tmp_path): + cr_checker = load_cr_checker_module() + script = tmp_path / "script.py" + script.write_text( + "#!/usr/bin/env python3\n\nprint('hi')\n", + encoding="utf-8", + ) + + offset = cr_checker.detect_shebang_offset(script, "utf-8") + + assert offset == len("#!/usr/bin/env python3\n\n".encode("utf-8")) + + +@pytest.fixture( + params=[ + "cpp", + "c", + "h", + "hpp", + "py", + "sh", + "bzl", + "ini", + "yml", + "yaml", + "BUILD", + "bazel", + "rs", + "rst", + ] +) +def prepare_test_with_header(request: SubRequest, tmp_path: PosixPath) -> tuple: + extension = request.param + test_file = tmp_path / ("file." + extension) + header_template = load_template(extension) + current_year = datetime.now().year + header = header_template.format(year=current_year, author="Author") + test_file.write_text( + header + "some content\n", + encoding="utf-8", + ) + return test_file, extension, header_template + + +@pytest.fixture( + params=[ + "cpp", + "c", + "h", + "hpp", + "py", + "sh", + "bzl", + "ini", + "yml", + "yaml", + "BUILD", + "bazel", + "rs", + "rst", + ] +) +def prepare_test_no_header(request: SubRequest, tmp_path: PosixPath) -> tuple: + extension = request.param + test_file = tmp_path / ("file." + extension) + header_template = load_template(extension) + current_year = datetime.now().year + test_file.write_text( + "some content\n", + encoding="utf-8", + ) + return test_file, extension, header_template, tmp_path + + +def test_process_files_detects_header(prepare_test_with_header): + cr_checker = load_cr_checker_module() + test_file, extension, header_template = prepare_test_with_header + + results = cr_checker.process_files( + [test_file], + {extension: header_template}, + False, + use_mmap=False, + encoding="utf-8", + offset=0, + remove_offset=0, + ) + + assert results["no_copyright"] == 0 + + +def test_process_files_detects_missing_header(prepare_test_no_header): + cr_checker = load_cr_checker_module() + test_file, extension, header_template, tmp_path = prepare_test_no_header + + results = cr_checker.process_files( + [test_file], + {extension: header_template}, + False, + use_mmap=False, + encoding="utf-8", + offset=0, + remove_offset=0, + ) + + assert results["no_copyright"] == 1 + + +def test_process_files_inserts_missing_header(prepare_test_no_header): + cr_checker = load_cr_checker_module() + test_file, extension, header_template, tmp_path = prepare_test_no_header + author = "Author" + config = write_config(tmp_path, author) + + results = cr_checker.process_files( + [test_file], + {extension: header_template}, + True, + config=config, + use_mmap=False, + encoding="utf-8", + offset=0, + remove_offset=0, + ) + + assert results["no_copyright"] == 1 + assert results["fixed"] == 1 + expected_header = header_template.format(year=datetime.now().year, author="Author") + assert test_file.read_text(encoding="utf-8").startswith(expected_header) + + +def test_process_files_skips_exclusion_with_missing_header(prepare_test_no_header): + cr_checker = load_cr_checker_module() + test_file, extension, header_template, tmp_path = prepare_test_no_header + + results = cr_checker.process_files( + [test_file], + {extension: header_template}, + False, + [str(test_file)], + use_mmap=False, + encoding="utf-8", + offset=0, + remove_offset=0, + ) + + assert results["no_copyright"] == 0 + + +# test that process_files function validates a license header after the shebang line +def test_process_files_accepts_header_after_shebang(tmp_path): + cr_checker = load_cr_checker_module() + script = tmp_path / "script.py" + header_template = load_template("py") + current_year = datetime.now().year + header = header_template.format(year=current_year, author="Author") + script.write_text( + "#!/usr/bin/env python3\n" + header + "print('hi')\n", + encoding="utf-8", + ) + + results = cr_checker.process_files( + [script], + {"py": header_template}, + False, + use_mmap=False, + encoding="utf-8", + offset=0, + remove_offset=0, + ) + + assert results["no_copyright"] == 0 + + +# test that process_files function fixes a missing license header after the shebang line +def test_process_files_fix_inserts_header_after_shebang(tmp_path): + cr_checker = load_cr_checker_module() + script = tmp_path / "script.py" + script.write_text( + "#!/usr/bin/env python3\nprint('hi')\n", + encoding="utf-8", + ) + header_template = load_template("py") + current_year = datetime.now().year + author = "Author" + config = write_config(tmp_path, author) + + results = cr_checker.process_files( + [script], + {"py": header_template}, + True, + config=config, + use_mmap=False, + encoding="utf-8", + offset=0, + remove_offset=0, + ) + + assert results["fixed"] == 1 + assert results["no_copyright"] == 1 + expected_header = header_template.format(year=current_year, author=author) + assert script.read_text(encoding="utf-8") == ( + "#!/usr/bin/env python3\n" + expected_header + "\n" + "print('hi')\n" + ) + + +# test that process_files function validates a license header without the shebang line +def test_process_files_accepts_header_without_shebang(tmp_path): + cr_checker = load_cr_checker_module() + script = tmp_path / "script.py" + header_template = load_template("py") + current_year = datetime.now().year + header = header_template.format(year=current_year, author="Author") + script.write_text(header + "print('hi')\n", encoding="utf-8") + + results = cr_checker.process_files( + [script], + {"py": header_template}, + False, + use_mmap=False, + encoding="utf-8", + offset=0, + remove_offset=0, + ) + + assert results["no_copyright"] == 0 + + +# test that process_files function fixes a missing license header without the shebang +def test_process_files_fix_inserts_header_without_shebang(tmp_path): + cr_checker = load_cr_checker_module() + script = tmp_path / "script.py" + script.write_text("print('hi')\n", encoding="utf-8") + header_template = load_template("py") + current_year = datetime.now().year + author = "Author" + config = write_config(tmp_path, author) + + results = cr_checker.process_files( + [script], + {"py": header_template}, + True, + config=config, + use_mmap=False, + encoding="utf-8", + offset=0, + remove_offset=0, + ) + + assert results["fixed"] == 1 + assert results["no_copyright"] == 1 + expected_header = header_template.format(year=current_year, author=author) + assert ( + script.read_text(encoding="utf-8") == expected_header + "\n" + "print('hi')\n" + ) + + +# test that border lines with different fill characters are accepted (flexible matching) +def test_process_files_accepts_flexible_border(tmp_path): + cr_checker = load_cr_checker_module() + test_file = tmp_path / "file.cpp" + current_year = datetime.now().year + # Use '/' fill chars instead of '*' for border lines + header = ( + "/////////////////////////////////////////////////////////////////////////////////////\n" + f" * Copyright (c) {current_year} Author\n" + " *\n" + " * See the NOTICE file(s) distributed with this work for additional\n" + " * information regarding copyright ownership.\n" + " *\n" + " * This program and the accompanying materials are made available under the\n" + " * terms of the Apache License Version 2.0 which is available at\n" + " * https://www.apache.org/licenses/LICENSE-2.0\n" + " *\n" + " * SPDX-License-Identifier: Apache-2.0\n" + " /////////////////////////////////////////////////////////////////////////////////////\n" + ) + test_file.write_text(header + "int main() {}\n", encoding="utf-8") + header_template = load_template("cpp") + + results = cr_checker.process_files( + [test_file], + {"cpp": header_template}, + False, + use_mmap=False, + encoding="utf-8", + offset=0, + remove_offset=0, + ) + + assert results["no_copyright"] == 0 + + +# test that a blank line after the header does not cause a check failure +def test_process_files_accepts_header_with_trailing_blank_line(tmp_path): + cr_checker = load_cr_checker_module() + test_file = tmp_path / "file.py" + header_template = load_template("py") + current_year = datetime.now().year + header = header_template.format(year=current_year, author="Author") + test_file.write_text(header + "\nsome content\n", encoding="utf-8") + + results = cr_checker.process_files( + [test_file], + {"py": header_template}, + False, + use_mmap=False, + encoding="utf-8", + offset=0, + remove_offset=0, + ) + + assert results["no_copyright"] == 0 + + +# test that fix_copyright inserts a blank line after the header +def test_process_files_fix_inserts_trailing_blank_line(tmp_path): + cr_checker = load_cr_checker_module() + test_file = tmp_path / "file.py" + test_file.write_text("some content\n", encoding="utf-8") + header_template = load_template("py") + current_year = datetime.now().year + author = "Author" + config = write_config(tmp_path, author) + + cr_checker.process_files( + [test_file], + {"py": header_template}, + True, + config=config, + use_mmap=False, + encoding="utf-8", + offset=0, + remove_offset=0, + ) + + expected_header = header_template.format(year=current_year, author=author) + assert test_file.read_text(encoding="utf-8").startswith(expected_header + "\n") + + +# test that has_duplicate_copyright detects a header that appears twice +def test_has_duplicate_copyright_detects_duplicate(tmp_path): + cr_checker = load_cr_checker_module() + test_file = tmp_path / "file.py" + header_template = load_template("py") + current_year = datetime.now().year + header = header_template.format(year=current_year, author="Author") + test_file.write_text(header + header + "some content\n", encoding="utf-8") + + result = cr_checker.has_duplicate_copyright( + test_file, header_template, False, "utf-8", 0 + ) + + assert result is True + + +# test that has_duplicate_copyright returns False for a single header +def test_has_duplicate_copyright_single_header(tmp_path): + cr_checker = load_cr_checker_module() + test_file = tmp_path / "file.py" + header_template = load_template("py") + current_year = datetime.now().year + header = header_template.format(year=current_year, author="Author") + test_file.write_text(header + "some content\n", encoding="utf-8") + + result = cr_checker.has_duplicate_copyright( + test_file, header_template, False, "utf-8", 0 + ) + + assert result is False + + +# test that process_files counts duplicate headers separately from missing headers +def test_process_files_detects_duplicate_header(tmp_path): + cr_checker = load_cr_checker_module() + test_file = tmp_path / "file.py" + header_template = load_template("py") + current_year = datetime.now().year + header = header_template.format(year=current_year, author="Author") + test_file.write_text(header + header + "some content\n", encoding="utf-8") + + results = cr_checker.process_files( + [test_file], + {"py": header_template}, + False, + use_mmap=False, + encoding="utf-8", + offset=0, + remove_offset=0, + ) + + assert results["duplicate_copyright"] == 1 + assert results["no_copyright"] == 0 + + +# test that has_duplicate_copyright detects two headers with different year ranges +def test_has_duplicate_copyright_detects_different_year_ranges(tmp_path): + cr_checker = load_cr_checker_module() + test_file = tmp_path / "file.py" + header_template = load_template("py") + header1 = header_template.format(year="2026", author="Author") + header2 = header_template.format(year="2024-2026", author="Author") + test_file.write_text(header1 + header2 + "some content\n", encoding="utf-8") + + result = cr_checker.has_duplicate_copyright( + test_file, header_template, False, "utf-8", 0 + ) + + assert result is True diff --git a/cr_checker/tool/BUILD b/cr_checker/tool/BUILD new file mode 100644 index 0000000..8ef719b --- /dev/null +++ b/cr_checker/tool/BUILD @@ -0,0 +1,21 @@ +# ******************************************************************************* +# 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("@aspect_rules_py//py:defs.bzl", "py_library") + +py_library( + name = "cr_checker_lib", + srcs = [ + "cr_checker.py", + ], + visibility = ["//visibility:public"], +) diff --git a/cr_checker/tool/cr_checker.py b/cr_checker/tool/cr_checker.py new file mode 100755 index 0000000..1cfe050 --- /dev/null +++ b/cr_checker/tool/cr_checker.py @@ -0,0 +1,904 @@ +#!/usr/bin/env python3 + +# ******************************************************************************* +# 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 checking if artifacts have proper copyright.""" + +import argparse +import json +import logging +import mmap +import os +import re +import shutil +import sys +import tempfile +from datetime import datetime +from pathlib import Path + +BYTES_TO_READ = 4 * 1024 +DEFAULT_AUTHOR = "Contributors to the Eclipse Foundation" + +BORDER_FILL_PATTERN = re.compile(r"([/*#'\-=+])\1{4,}") +FILL_CHARS_REGEX = r"[/*#'\-=+]+" + +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) + + +class ParamFileAction(argparse.Action): # pylint: disable=too-few-public-methods + """ + A custom argparse action to support exclusive parameter files for command-line arguments. + + The `ParamFileAction` class allows users to specify a parameter file (prefixed with '@') + containing file paths or other inputs, which will override any additional inputs provided + in the command line. If a parameter file is found, its contents are used exclusively, + and all other inputs are ignored. If no parameter file is provided, standard inputs are used. + + Attributes: + parser (argparse.ArgumentParser): The argument parser instance. + namespace (argparse.Namespace): The namespace where arguments are stored. + values (list): The list of argument values passed from the command line. + option_string (str, optional): The option string that triggered this action, if any. + + Methods: + __call__(parser, namespace, values, option_string=None): Processes the arguments. + - If any value starts with '@', it reads the parameter file and sets `file_paths` + in `namespace`. + - If no parameter file is detected, it directly assigns `values` to `namespace`. + """ + + def __call__(self, parser, namespace, values, option_string=None): + paramfile = next((v[1:] for v in values if v.startswith("@")), None) + if paramfile: + with open(paramfile, "r", encoding="utf-8") as handle: + file_paths = [line.strip() for line in handle if line.strip()] + setattr(namespace, self.dest, file_paths) + else: + setattr(namespace, self.dest, values) + + +def get_author_from_config(config_path: Path = None) -> str: + """ + Reads the author from a JSON configuration file. + + Args: + config_path (Path): Path to the configuration JSON file. + + Returns: + str: Author from the configuration file. + """ + if not config_path: + return DEFAULT_AUTHOR + with config_path.open("r") as file: + config = json.load(file) + return config.get("author", DEFAULT_AUTHOR) + + +def convert_bre_to_regex(template: str) -> str: + """ + Convert BRE-style template (literal by default) to standard regex. + In the template: * is literal, \\* is a metacharacter. + """ + # First, escape all regex metacharacters to make them literal + escaped = re.escape(template) + # Now, find escaped backslashes followed by escaped metacharacters + # and convert them back to actual regex metacharacters + metacharacters = r"\\.*+-?[]{}()^$|" + for char in metacharacters: + escaped = escaped.replace(re.escape("\\" + char), char) + return escaped + + +def line_to_flexible_regex(line: str) -> str: + """ + Convert a border line to a regex that accepts any fill characters. + + Runs of 5+ identical fill characters (e.g. ``****``) are replaced with + ``[/*#'\\-=+]+`` so that alternative styles (e.g. ``////``) are also + accepted. + """ + stripped = line.rstrip("\n") + has_newline = line.endswith("\n") + result = [] + last_end = 0 + for m in BORDER_FILL_PATTERN.finditer(stripped): + result.append(re.escape(stripped[last_end : m.start()])) + result.append(FILL_CHARS_REGEX) + last_end = m.end() + result.append(re.escape(stripped[last_end:])) + if has_newline: + result.append("\n") + return "".join(result) + + +def load_templates(path): + """ + Loads the copyright templates from a configuration file. + + Args: + path (str): Path to the template file. + + Returns: + dict: A dictionary where each key is a file extension (e.g., ".cpp") + and the value is the template string from the config. + """ + + def add_template_for_extensions(templates: dict, extensions: list, template: str): + # Remove trailing lines from template and ensure line end + template = template.rstrip() + "\n" + for extension in extensions: + templates[extension] = template + + templates = {} + current_extensions = [] + + with open(path, "r", encoding="utf-8") as file: + lines = file.readlines() + template_for_extensions = "" + + for line in lines: + stripped_line = line.strip() + + if stripped_line.startswith("[") and stripped_line.endswith("]"): + add_template_for_extensions( + templates, current_extensions, template_for_extensions + ) + + template_for_extensions = "" + + extensions = stripped_line[1:-1].split(",") + current_extensions = [ext.strip() for ext in extensions] + LOGGER.debug(current_extensions) + else: + template_for_extensions += line + + add_template_for_extensions( + templates, current_extensions, template_for_extensions + ) + + LOGGER.debug(templates) + return templates + + +def load_exclusion(path): + """ + Loads the list of files being excluded from the copyright check. + + Args: + path (str): Path to the exclusion file. + + Returns: + tuple(list, bool): a list of files that are excluded from the copyright check and a boolean indicating whether + all paths listed in the exclusion file exist and are files. + """ + + exclusion = [] + valid = True + with open(path, "r", encoding="utf-8") as file: + exclusion = file.read().splitlines() + + for item in exclusion: + path = Path(item) + if not path.exists(): + LOGGER.error("Excluded file %s does not exist.", item) + exclusion.remove(item) + valid = False + continue + if not path.is_file(): + exclusion.remove(item) + LOGGER.error("Excluded file %s is not a file.", item) + valid = False + continue + + LOGGER.debug(exclusion) + return exclusion, valid + + +def configure_logging(log_file_path=None, verbose=False): + """ + Configures logging to write messages to the specified log file. + + Args: + log_file_path (str, optional): Path to the log file. + verbose (bool, optional): If True, sets log level to DEBUG. Otherwise, sets it to INFO. + """ + 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 detect_shebang_offset(path, encoding): + """ + Detects if a file starts with a shebang (#!) and returns the byte offset + to skip it (length of the first line including newline). + + Args: + path (Path): A `pathlib.Path` object pointing to the file. + encoding (str): Encoding type to use when reading the file. + + Returns: + int: The byte length of the shebang line (including newline) if present, + otherwise 0. + """ + try: + with open(path, "r", encoding=encoding) as handle: + first_line = handle.readline() + if first_line.startswith("#!"): + # Calculate byte length of the first line + byte_length = len(first_line.encode(encoding)) + while True: + next_char = handle.read(1) + if not next_char or next_char not in ("\n", "\r"): + break + byte_length += len(next_char.encode(encoding)) + LOGGER.debug( + "Detected shebang in %s with offset %d bytes", path, byte_length + ) + return byte_length + except (IOError, OSError) as err: + LOGGER.debug("Could not detect shebang in %s: %s", path, err) + return 0 + + +def load_text_from_file(path, header_length, encoding, offset): + """ + Reads the first portion of a file, up to `header_length` characters + plus an additional offset if provided. + + Args: + path (Path): A `pathlib.Path` object pointing to the file. + header_length (int): Number of characters to read for the header. + encoding (str): Encoding type to use when reading the file. + offset (int): Additional number of characters to read beyond + `header_length`, typically used to account for extra + lines (such as a shebang) before the header. + + Returns: + str: The portion of the file read, which should contain the header if present, + including any extra characters specified by `offset`. + """ + total_length = header_length + offset + LOGGER.debug( + "Reading first %d characters from file: %s [%s]", total_length, path, encoding + ) + with open(path, "r", encoding=encoding) as handle: + content = handle.read(total_length) + return content[offset:] if offset else content + + +def load_text_from_file_with_mmap(path, header_length, encoding, offset): + """ + Maps the file and reads only the first `header_length` bytes plus + an additional offset if provided. + + Args: + path (Path): A `pathlib.Path` object pointing to the file. + header_length (int): Length of the header text to check. + encoding (str): String for setting decoding type. + offset (int): Additional number of characters to read beyond + `header_length`, typically used to account for extra + lines (such as a shebang) before the header. + + Returns: + str: The portion of the file read, which should contain the header if present. + """ + + file_size = os.path.getsize(path) + total_length = header_length + offset + length = min(total_length, file_size) + + if not length: + LOGGER.warning( + "File %s is empty [length: %d]. Return empty string.", path, length + ) + return "" + + LOGGER.debug("Memory mapping first %d bytes from file: %s", total_length, path) + with open(path, "r", encoding=encoding) as handle: + with mmap.mmap(handle.fileno(), length=length, access=mmap.ACCESS_READ) as fmap: + return fmap[:length].decode(encoding)[offset:] + + +def has_copyright(path, template, use_mmap, encoding, offset, config=None): + """ + Checks if the specified copyright text is present in the beginning of a file. + + Args: + path (Path): A `pathlib.Path` object pointing to the file to check. + template (str): The copyright text to search for at the beginning + of the file. + use_mmap (bool): If True, uses memory-mapped file reading for efficient + large file handling. + encoding (str): Encoding type to use when reading the file. + offset (int): Additional number of characters to read beyond the length + of `copyright_text`, used to account for extra content + (such as a shebang) before the copyright text. + config (Path): Path to the config JSON file where configuration + variables are stored (e.g. years for copyright headers). + + Returns: + bool: True if the file contains the copyright text, False if it is missing. + + Raises: + IOError: If there is an error opening or reading the file. + """ + + load_text = load_text_from_file_with_mmap if use_mmap else load_text_from_file + + lines = template.splitlines(keepends=True) + regex_parts = [] + for line in lines: + stripped_line = line.rstrip("\n") + if BORDER_FILL_PATTERN.search(stripped_line): + regex_parts.append(line_to_flexible_regex(line)) + else: + formatted = line.format(year=r"\\d\{4\}\(-\\d\{4\}\)\?", author=r"\.\*") + regex_parts.append(convert_bre_to_regex(formatted)) + template_regex = "".join(regex_parts) + "\n?" + + if re.match(template_regex, load_text(path, BYTES_TO_READ, encoding, offset)): + LOGGER.debug("File %s has copyright.", path) + return True + + LOGGER.debug("File %s doesn't have copyright.", path) + return False + + +def has_any_copyright(path, use_mmap, encoding, offset): + """ + Checks if any copyright notice is present in the file header, regardless of format. + + Args: + path (Path): A `pathlib.Path` object pointing to the file to check. + use_mmap (bool): If True, uses memory-mapped file reading. + encoding (str): Encoding type to use when reading the file. + offset (int): Byte offset to skip (e.g. shebang line). + + Returns: + bool: True if any copyright notice is found, False otherwise. + """ + load_text = load_text_from_file_with_mmap if use_mmap else load_text_from_file + content = load_text(path, BYTES_TO_READ, encoding, offset) + return bool( + re.search( + r"Copyright.*SPDX-License-Identifier", content, re.IGNORECASE | re.DOTALL + ) + ) + + +def has_duplicate_copyright(path, template, use_mmap, encoding, offset): + """ + Checks if more than one copyright notice is present in the file header. + + The check is format-agnostic: it counts occurrences of ``SPDX-License-Identifier`` + within a window of twice the template length, so that headers written by different + tools (e.g. REUSE vs. cr_checker) are both counted while string literals that + embed copyright text further into the file are ignored. + + Args: + path (Path): A `pathlib.Path` object pointing to the file to check. + template (str): The copyright template; its length defines the search window. + use_mmap (bool): If True, uses memory-mapped file reading. + encoding (str): Encoding type to use when reading the file. + offset (int): Byte offset to skip (e.g. shebang line). + + Returns: + bool: True if more than one copyright notice is found, False otherwise. + """ + load_text = load_text_from_file_with_mmap if use_mmap else load_text_from_file + content = load_text(path, 2 * len(template), encoding, offset) + matches = list(re.finditer(r"SPDX-License-Identifier", content, re.IGNORECASE)) + if len(matches) > 1: + LOGGER.debug("File %s has %d copyright headers.", path, len(matches)) + return True + return False + + +def get_files_from_dir(directory, exts=None): + """ + Finds files in the specified directories. Filters by extensions if provided. + + Args: + dirs (list of str): List of directories to search for files. + exts (list of str, optional): List of extensions to filter files. + If None, all files are returned. + + Returns: + list of str: List of file paths found in the directories. + """ + collected_files = [] + LOGGER.debug("Getting files from directory: %s", directory) + for path in directory.rglob("*"): + if path.is_file() and path.stat().st_size != 0: + if ( + exts is None + or path.suffix[1:] in exts + or (path.name == "BUILD" and "BUILD" in exts) + ): + collected_files.append(path) + return collected_files + + +def collect_inputs(inputs, exts=None): + """ + Collects files from a list of input paths, optionally filtering by file extensions. + + Args: + inputs (list): A list of paths to files or directories. + If a directory is provided, all files within it are added to the output. + exts (list, optional): A list of file extensions to filter by (e.g., ['.py', '.txt']). + Only files with these extensions will be included if specified. + + Returns: + list: A list of file paths collected from the input paths, filtered by the given extensions. + If an input is neither a file nor a directory, it is skipped with a warning. + + Logs: + Logs messages at the DEBUG level, detailing processing of directories and files, + and warns if an invalid input path is encountered. + """ + all_files = [] + LOGGER.debug("Extensions: %s", exts) + for i in inputs: + item = Path(i) + if item.is_dir(): + LOGGER.debug("Processing directory: %s", item) + all_files.extend(get_files_from_dir(item, exts)) + elif item.is_file() and ( + exts is None + or item.suffix[1:] in exts + or (item.name == "BUILD" and "BUILD" in exts) + ): + LOGGER.debug("Processing file: %s", item) + all_files.append(item) + elif item.is_file(): + LOGGER.debug("Skipped (no configuration for file extension): %s", item) + else: + LOGGER.warning("Skipped (input is not a valid file or directory): %s", item) + return all_files + + +def create_temp_file(path, encoding): + """ + Creates a temporary file with the provided content. + + Args: + path (str): The path of file to write the content to the temporary file. + encoding (str, optional): Encoding type to use when writing the file. + + Returns: + str: The path to the temporary file created. + """ + with tempfile.NamedTemporaryFile(mode="w", encoding=encoding, delete=False) as temp: + with open(path, "r", encoding=encoding) as handle: + for chunk in iter(lambda: handle.read(4096), ""): + temp.write(chunk) + return temp.name + + +def remove_old_header(file_path, encoding, num_of_chars): + """ + Removes the first `num_of_chars` characters from a file and updates it in-place. + + Args: + file_path (str): Path to the file to be modified. + encoding (str): Encoding used to read and write the file. + num_of_chars (int): Number of characters to remove from the beginning of the file. + + Raises: + IOError: If there is an issue reading or writing the file. + ValueError: If `num_of_chars` is negative. + """ + with open(file_path, "r", encoding=encoding) as file: + file.seek(num_of_chars) + with tempfile.NamedTemporaryFile( + "w", delete=False, encoding=encoding + ) as temp_file: + shutil.copyfileobj(file, temp_file) + shutil.move(temp_file.name, file_path) + + +def fix_copyright(path, copyright_text, encoding, offset, config=None) -> bool: + """ + Inserts a copyright header into the specified file, ensuring that existing + content is preserved according to the provided offset. + + Args: + path (str): The path to the file that needs the copyright header. + copyright_text (str): The copyright text to be added. + encoding (str): The character encoding used to read and write the file. + offset (int): The number of bytes to preserve at the top of the file. + If 0, the first line is overwritten unless it's empty. + For non-zero offsets, ensures the correct number of bytes + are preserved. + config (Path): Path to the config JSON file where configuration + variables are stored (e.g. years for copyright headers). + Returns: + bool: True if the copyright header was successfully added, False if there was an error + """ + + temporary_file = create_temp_file(path, encoding) + + with open(temporary_file, "r", encoding=encoding) as temp: + first_line = temp.readline() + byte_array = len(first_line.encode(encoding)) + + if offset > 0 and offset != byte_array: + LOGGER.error( + "%s: Invalid offset value: %d, expected: %d", path, offset, byte_array + ) + return False + + with open(path, "w", encoding=encoding) as handle: + temp.seek(0) + if offset > 0: + handle.write(first_line) + temp.seek(offset) + handle.write( + copyright_text.format( + year=datetime.now().year, author=get_author_from_config(config) + ) + + "\n" + ) + for chunk in iter(lambda: temp.read(4096), ""): + handle.write(chunk) + LOGGER.info("Fixed missing header in: %s", path) + return True + + +def process_files( + files, + templates, + fix, + exclusion=[], + config=None, + use_mmap=False, + encoding="utf-8", + offset=0, + remove_offset=0, +): # pylint: disable=too-many-arguments + """ + Processes a list of files to check for the presence of copyright text. + + Args: + files (list): A list of file paths to check. + templates (dict): A dictionary where keys are file extensions + (e.g., '.py', '.txt') and values are strings or patterns + representing the required copyright text. + exclusion (list): A list of paths to files to be excluded from the copyright + check. + config (Path): Path to the config JSON file where configuration + variables are stored (e.g. years for copyright headers). + use_mmap (bool): Flag for using mmap function for reading files + (instead of standard option). + encoding (str): Encoding type to use when reading the file. + offset (int): Additional number of characters to read beyond the length + of `copyright_text`, used to account for extra content + (such as a shebang) before the copyright text. + remove_offset(int): Flag for removing old header from source files + (before applying the new one) in number of chars. + + Returns: + int: The number of files that do not contain the required copyright text. + """ + results = {"no_copyright": 0, "fixed": 0, "duplicate_copyright": 0} + for item in files: + name = Path(item).name + key = name if name == "BUILD" else Path(item).suffix[1:] + if key not in templates.keys(): + logging.debug( + "Skipped (no configuration for selected file extension): %s", item + ) + continue + + if str(item) in exclusion: + logging.debug("Skipped due to exclusion: %s", item) + continue + + if os.path.getsize(item) == 0: + # No need to add copyright headers to empty files + continue + + # Automatically detect shebang and use its offset if no manual offset provided + shebang_offset = detect_shebang_offset(item, encoding) + effective_offset = offset + shebang_offset if offset == 0 else offset + + if has_duplicate_copyright( + item, templates[key], use_mmap, encoding, effective_offset + ): + LOGGER.error("Duplicate copyright header in: %s", item) + results["duplicate_copyright"] += 1 + elif not has_copyright( + item, templates[key], use_mmap, encoding, effective_offset, config + ): + if has_any_copyright(item, use_mmap, encoding, effective_offset): + LOGGER.warning( + "Wrong copyright format in: %s, expected format from template", item + ) + elif fix: + if remove_offset: + remove_old_header(item, encoding, remove_offset) + fix_result = fix_copyright( + item, templates[key], encoding, effective_offset, config + ) + results["no_copyright"] += 1 + if fix_result: + results["fixed"] += 1 + else: + LOGGER.error( + "Missing copyright header in: %s, use --fix to introduce it", item + ) + results["no_copyright"] += 1 + return results + + +def parse_arguments(argv): + """ + Parses command-line arguments. + + Args: + argv (list of str): List of command-line arguments. + + Returns: + argparse.Namespace: Parsed arguments containing files, directories, + copyright_file, extensions and log_file. + """ + parser = argparse.ArgumentParser( + description="A script to check for copyright in files with specific extensions." + ) + + parser.add_argument( + "-t", + "--template-file", + type=Path, + required=True, + help="Path to the template file", + ) + + parser.add_argument( + "--exclusion-file", + type=Path, + required=False, + help="Path to the file listing file paths excluded from the copyright check.", + ) + + parser.add_argument( + "-c", + "--config-file", + type=Path, + default=None, + help="Path to the config file", + ) + + 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( + "-e", + "--extensions", + type=str, + nargs="+", + default=None, + help="List of extensions to filter when searching for files, e.g., '.h .cpp'", + ) + + parser.add_argument( + "--use_memory_map", + action="store_true", + help="Use memory map for reading content of files \ + (should be used reading gigabyte ranged files).", + ) + + parser.add_argument( + "-f", + "--fix", + action="store_true", + help="Fix missing copyright headers by inserting them", + ) + + parser.add_argument( + "--encoding", default="utf-8", help="File encoding (default: utf-8)." + ) + + parser.add_argument( + "--offset", + dest="offset", + type=int, + default=0, + help="Additional length offset to account for characters like a shebang (default is 0)", + ) + + parser.add_argument( + "--remove-offset", + dest="remove_offset", + type=int, + default=0, + help="Offset to remove old header from beginning of the file \ + (supported only with --fix mode)", + ) + + parser.add_argument( + "inputs", + nargs="+", + action=ParamFileAction, + help="Directories and/or files to parse.", + ) + + return parser.parse_args(argv) + + +def main(argv=None): + """ + Entry point for processing files to check for the presence of required copyright text. + + This function parses command-line arguments, configures logging, loads copyright templates, + collects input files based on provided criteria, and checks each file for the required + copyright text. + + Args: + argv (list, optional): List of command-line arguments. + If `None`, defaults to `sys.argv[1:]`. + + Returns: + int: Error code if an IOError occurs during loading templates or collecting input files; + otherwise, returns 0 as success. + """ + args = parse_arguments(argv if argv is not None else sys.argv[1:]) + configure_logging(args.log_file, args.verbose) + + try: + templates = load_templates(args.template_file) + except IOError as err: + LOGGER.error("Failed to load copyright text: %s", err) + return err.errno + + exclusion = [] + exclusion_valid = True + if args.exclusion_file: + try: + exclusion, exclusion_valid = load_exclusion(args.exclusion_file) + except IOError as err: + LOGGER.error("Failed to load exclusion list: %s", err) + return err.errno + + try: + files = collect_inputs(args.inputs, args.extensions) + except IOError as err: + LOGGER.error("Failed to process file %s with error", err.filename) + return err.errno + + LOGGER.debug("Running check on files: %s", files) + + if args.fix and args.remove_offset: + LOGGER.info("%s!------DANGER ZONE------!%s", COLORS["RED"], COLORS["ENDC"]) + LOGGER.info("Remove offset set! This can REMOVE parts of source files!") + LOGGER.info( + "Use ONLY if invalid copyright header is present that needs to be removed!" + ) + LOGGER.info("%s!-----------------------!%s", COLORS["RED"], COLORS["ENDC"]) + + results = process_files( + files, + templates, + args.fix, + exclusion, + args.config_file, + args.use_memory_map, + args.encoding, + args.offset, + args.remove_offset, + ) + total_no = results["no_copyright"] + total_fixes = results["fixed"] + total_duplicates = results["duplicate_copyright"] + + LOGGER.info("=" * 64) + LOGGER.info("Process completed.") + LOGGER.info( + "Total files without copyright: %s%d%s", + COLORS["RED"] if total_no > 0 else COLORS["GREEN"], + total_no, + COLORS["ENDC"], + ) + LOGGER.info( + "Total files with duplicate copyright: %s%d%s", + COLORS["RED"] if total_duplicates > 0 else COLORS["GREEN"], + total_duplicates, + COLORS["ENDC"], + ) + if not exclusion_valid: + LOGGER.info("The exclusion file contains paths that do not exist.") + if args.fix: + total_not_fixed = total_no - total_fixes + LOGGER.info( + "Total files that were fixed: %s%d%s", + COLORS["GREEN"], + total_fixes, + COLORS["ENDC"], + ) + LOGGER.info( + "Total files that were NOT fixed: %s%d%s", + COLORS["RED"] if total_not_fixed > 0 else COLORS["GREEN"], + total_not_fixed, + COLORS["ENDC"], + ) + LOGGER.info("=" * 64) + + return 0 if (total_no == 0 and total_duplicates == 0 and exclusion_valid) else 1 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/cr_checker/tool/pre-commit_wrapper b/cr_checker/tool/pre-commit_wrapper new file mode 100755 index 0000000..90f1f9d --- /dev/null +++ b/cr_checker/tool/pre-commit_wrapper @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 + +# ******************************************************************************* +# 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 checking if artifacts have proper copyright.""" + +import cr_checker +import os +import sys + +if __name__ == "__main__": + # --template-file and --config-file cannot be set via .pre-commit-config.yaml + script_dir = os.path.dirname(os.path.abspath(__file__)) + sys.argv.insert(1, '--template-file') + sys.argv.insert( + 2, + os.path.normpath(os.path.join(script_dir, '..', 'resources', 'templates.ini')), + ) + sys.argv.insert(3, '--config-file') + sys.argv.insert( + 4, + os.path.normpath(os.path.join(script_dir, '..', 'resources', 'config.json')), + ) + sys.argv.insert(5, '--exclusion-file') + sys.argv.insert( + 6, + os.path.normpath(os.path.join(script_dir, '..', 'resources', 'exclusion.txt')), + ) + + sys.exit(cr_checker.main(sys.argv[1:])) From d44a44753ab0ab6225eab757c630e07424cd10d0 Mon Sep 17 00:00:00 2001 From: MaximilianSoerenPollak Date: Wed, 15 Jul 2026 12:09:32 +0200 Subject: [PATCH 02/22] Chore: fixing various things (linting & formatting) Fix copyright Fix reuse copyright findingds Fix linting --- .bazelrc | 1 - .github/workflows/tests.yml | 54 ++++++++++++++--------------- .pre-commit-config.yaml | 12 +++++++ cr_checker/tests/test_cr_checker.py | 2 +- 4 files changed, 40 insertions(+), 29 deletions(-) diff --git a/.bazelrc b/.bazelrc index 2d44ee2..1b8cd6c 100644 --- a/.bazelrc +++ b/.bazelrc @@ -38,4 +38,3 @@ build:debug --//bazel/rules/rules_score:verbosity=debug coverage:coverage --combined_report=lcov #coverage:coverage --@rules_rust//rust/settings:extra_rustc_flag=-Clink-dead-code #coverage:coverage --@rules_rust//rust/settings:extra_rustc_flag=-Ccodegen-units=1 - diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 44232e6..5a43bd0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,30 +10,30 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* - integration_tests: - runs-on: ubuntu-24.04 - permissions: - contents: read - actions: write - steps: - - name: Checkout repository - uses: actions/checkout@v7.0.0 - - name: Free Disk Space (Ubuntu) - uses: eclipse-score/more-disk-space@v1 - with: - level: 4 - - uses: castler/setup-bazel@cache-optimized - with: - bazelisk-cache: true - disk-cache: integration_tests - repository-cache: true - cache-optimized: true - cache-save: ${{ github.ref == 'refs/heads/main' }} - # - name: Run python_basics integration tests - # run: | - # cd python_basics/integration_tests - # bazel test //... - - name: Run cr_checker unit tests - run: | - cd cr_checker/tests - bazel test //... +integration_tests: + runs-on: ubuntu-24.04 + permissions: + contents: read + actions: write + steps: + - name: Checkout repository + uses: actions/checkout@v7.0.0 + - name: Free Disk Space (Ubuntu) + uses: eclipse-score/more-disk-space@v1 + with: + level: 4 + - uses: castler/setup-bazel@cache-optimized + with: + bazelisk-cache: true + disk-cache: integration_tests + repository-cache: true + cache-optimized: true + cache-save: ${{ github.ref == 'refs/heads/main' }} + # - name: Run python_basics integration tests + # run: | + # cd python_basics/integration_tests + # bazel test //... + - name: Run cr_checker unit tests + run: | + cd cr_checker/tests + bazel test //... diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d2a5d82..e6c8436 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,15 @@ +# ******************************************************************************* +# Copyright (c) 2026 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 +# ******************************************************************************* repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # v6.0.0 diff --git a/cr_checker/tests/test_cr_checker.py b/cr_checker/tests/test_cr_checker.py index 48bc842..1d9a9ee 100644 --- a/cr_checker/tests/test_cr_checker.py +++ b/cr_checker/tests/test_cr_checker.py @@ -324,7 +324,7 @@ def test_process_files_accepts_flexible_border(tmp_path): " * terms of the Apache License Version 2.0 which is available at\n" " * https://www.apache.org/licenses/LICENSE-2.0\n" " *\n" - " * SPDX-License-Identifier: Apache-2.0\n" + " * SPDX-" "License-Identifier: Apache-2.0\n" " /////////////////////////////////////////////////////////////////////////////////////\n" ) test_file.write_text(header + "int main() {}\n", encoding="utf-8") From d9f5944e7ab41f43bf0d7aa74ed55ecfb936c2ee Mon Sep 17 00:00:00 2001 From: MaximilianSoerenPollak Date: Wed, 15 Jul 2026 15:08:25 +0200 Subject: [PATCH 03/22] fix: Delete 'config' file as no longer needed --- .github/workflows/tests.yml | 7 +---- cr_checker/cr_checker.bzl | 5 --- cr_checker/resources/BUILD | 16 ---------- cr_checker/resources/config.json | 3 -- cr_checker/resources/exclusion.txt | 1 - cr_checker/resources/templates.ini | 10 +++--- cr_checker/tests/test_cr_checker.py | 45 ++++++++------------------- cr_checker/tool/cr_checker.py | 48 +++-------------------------- 8 files changed, 24 insertions(+), 111 deletions(-) delete mode 100644 cr_checker/resources/config.json delete mode 100644 cr_checker/resources/exclusion.txt diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5a43bd0..2ba5e3d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,10 +18,6 @@ integration_tests: steps: - name: Checkout repository uses: actions/checkout@v7.0.0 - - name: Free Disk Space (Ubuntu) - uses: eclipse-score/more-disk-space@v1 - with: - level: 4 - uses: castler/setup-bazel@cache-optimized with: bazelisk-cache: true @@ -35,5 +31,4 @@ integration_tests: # bazel test //... - name: Run cr_checker unit tests run: | - cd cr_checker/tests - bazel test //... + uv run pytest cr_checker/tests/ diff --git a/cr_checker/cr_checker.bzl b/cr_checker/cr_checker.bzl index c567554..b0e7693 100644 --- a/cr_checker/cr_checker.bzl +++ b/cr_checker/cr_checker.bzl @@ -20,7 +20,6 @@ def copyright_checker( srcs, visibility, template, - config, exclusion = None, extensions = [], offset = 0, @@ -40,8 +39,6 @@ def copyright_checker( targets can use this rule. template (str, optional): Path to the template resource used for validation. Defaults to "//tools/cr_checker/resources:templates". - config (str, optional): Path to the config resource used for project variables. - Defaults to "//tools/cr_checker/resources:config". exclusion (str, optional): Path to a text file listing files to be excluded from the copyright check. File format: one path per line, relative to the repository root. extensions (list, optional): A list of file extensions to filter the source files. @@ -67,7 +64,6 @@ def copyright_checker( args = [ "-t $(location {})".format(template), - "-c $(location {})".format(config), ] if len(extensions): args.append("-e {exts}".format( @@ -97,7 +93,6 @@ def copyright_checker( data = srcs[:] data.append(template) - data.append(config) if exclusion: data.append(exclusion) diff --git a/cr_checker/resources/BUILD b/cr_checker/resources/BUILD index 307734e..e11a316 100644 --- a/cr_checker/resources/BUILD +++ b/cr_checker/resources/BUILD @@ -18,19 +18,3 @@ filegroup( ], visibility = ["//visibility:public"], ) - -filegroup( - name = "config", - srcs = [ - "config.json", - ], - visibility = ["//visibility:public"], -) - -filegroup( - name = "exclusion", - srcs = [ - "exclusion.txt", - ], - visibility = ["//visibility:public"], -) diff --git a/cr_checker/resources/config.json b/cr_checker/resources/config.json deleted file mode 100644 index 285c132..0000000 --- a/cr_checker/resources/config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "author": "Contributors to the Eclipse Foundation" -} diff --git a/cr_checker/resources/exclusion.txt b/cr_checker/resources/exclusion.txt deleted file mode 100644 index 73d6ffc..0000000 --- a/cr_checker/resources/exclusion.txt +++ /dev/null @@ -1 +0,0 @@ -cr_checker/resources/templates.ini diff --git a/cr_checker/resources/templates.ini b/cr_checker/resources/templates.ini index d9f1220..3bc4f88 100644 --- a/cr_checker/resources/templates.ini +++ b/cr_checker/resources/templates.ini @@ -12,7 +12,7 @@ # ******************************************************************************* [cpp,c,h,hpp,trlc,rsl] /******************************************************************************** - * Copyright (c) {year} {author} + * Copyright (c) {year} Contributors to the Eclipse Foundation * * See the NOTICE file(s) distributed with this work for additional * information regarding copyright ownership. @@ -25,7 +25,7 @@ ********************************************************************************/ [py,sh,bzl,ini,yml,yaml,BUILD,bazel] # ******************************************************************************* -# Copyright (c) {year} {author} +# Copyright (c) {year} Contributors to the Eclipse Foundation # # See the NOTICE file(s) distributed with this work for additional # information regarding copyright ownership. @@ -39,7 +39,7 @@ [rst] .. # ******************************************************************************* - # Copyright (c) {year} {author} + # Copyright (c) {year} Contributors to the Eclipse Foundation # # See the NOTICE file(s) distributed with this work for additional # information regarding copyright ownership. @@ -52,7 +52,7 @@ # ******************************************************************************* [rs] // ******************************************************************************* -// Copyright (c) {year} {author} +// Copyright (c) {year} Contributors to the Eclipse Foundation // // See the NOTICE file(s) distributed with this work for additional // information regarding copyright ownership. @@ -78,7 +78,7 @@ ' ******************************************************************************* [md] # Tools diff --git a/REUSE.toml b/REUSE.toml deleted file mode 100644 index 6c94d54..0000000 --- a/REUSE.toml +++ /dev/null @@ -1,49 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2026 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 -# ******************************************************************************* - -version = 1 - -[[annotations]] -path = [".devcontainer/*.json"] -SPDX-FileCopyrightText = "Copyright (c) 2026 Contributors to the Eclipse Foundation" -SPDX-License-Identifier = "Apache-2.0" - -[[annotations]] -path = [".vscode/*.json"] -SPDX-FileCopyrightText = "Copyright (c) 2026 Contributors to the Eclipse Foundation" -SPDX-License-Identifier = "Apache-2.0" - -[[annotations]] -path = ["**/*.lock.json"] -SPDX-FileCopyrightText = "Copyright (c) 2026 Contributors to the Eclipse Foundation" -SPDX-License-Identifier = "Apache-2.0" - -[[annotations]] -path = ["**/requirements_lock.txt"] -SPDX-FileCopyrightText = "Copyright (c) 2026 Contributors to the Eclipse Foundation" -SPDX-License-Identifier = "Apache-2.0" - - -[[annotations]] -path = ["cr_checker/resources/config.json", - "**/.bazelversion", - "coverage/tests/fixtures/symbol_report.json", - "python_basics/.bazelversion", - "python_basics/integration_tests/venv-with-extra-requirements/requirements.in", - "python_basics/requirements.in", - "python_basics/requirements.txt", - "bazel/rules/rules_score/test/requirements.in", - "bazel/rules/rules_score/test/requirements.txt" - ] -SPDX-FileCopyrightText = "Copyright (c) 2026 Contributors to the Eclipse Foundation" -SPDX-License-Identifier = "Apache-2.0" diff --git a/cr_checker/cr_checker.bzl b/cr_checker/cr_checker.bzl index b0e7693..a23fd9d 100644 --- a/cr_checker/cr_checker.bzl +++ b/cr_checker/cr_checker.bzl @@ -45,8 +45,6 @@ def copyright_checker( Defaults to an empty list, meaning all files are checked. offset (int, optional): The line offset for applying checks or modifications. Defaults to 0. - remove_offset (int, optional): The line offset for removing chars from beginning of file. - Defaults to 0. debug (bool, optional): Whether to enable debug mode, providing additional logs. Defaults to False. use_memory_map (bool, optional): Whether to use memory mapping for large files to @@ -88,8 +86,6 @@ def copyright_checker( for t_name in t_names: if t_name == "{}.fix".format(name): args.insert(0, "--fix") - if remove_offset: - args.append("--remove_offset {}".format(remove_offset)) data = srcs[:] data.append(template) diff --git a/cr_checker/tests/MODULE.bazel b/cr_checker/tests/MODULE.bazel deleted file mode 100644 index 57b285e..0000000 --- a/cr_checker/tests/MODULE.bazel +++ /dev/null @@ -1,48 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2025 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 -# ******************************************************************************* -module( - name = "score_cr_checker_tests", - version = "0.1.0", -) - -# PYTHON -bazel_dep(name = "rules_python", version = "1.4.1") - -PYTHON_VERSION = "3.12" - -python = use_extension("@rules_python//python/extensions:python.bzl", "python") -python.toolchain( - python_version = PYTHON_VERSION, -) -use_repo(python) - -# PIP -pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") -pip.parse( - hub_name = "pip_deps_test", - python_version = PYTHON_VERSION, - requirements_lock = "//:requirements_lock.txt", -) -use_repo(pip, "pip_deps_test") - -bazel_dep(name = "score_utils", version = "0.0.0") -local_path_override( - module_name = "score_utils", - path = "../../", -) -bazel_dep(name = "score_tooling", version = "1.2.0") - -bazel_dep(name = "rules_rust", version = "0.68.1-score") -bazel_dep(name = "rules_shell", version = "0.5.0") - -# end Tests diff --git a/cr_checker/tests/requirements_lock.txt b/cr_checker/tests/requirements_lock.txt deleted file mode 100644 index e504a61..0000000 --- a/cr_checker/tests/requirements_lock.txt +++ /dev/null @@ -1 +0,0 @@ -bazel-runfiles==1.3.0 \ No newline at end of file diff --git a/cr_checker/tests/test_cr_checker.py b/cr_checker/tests/test_cr_checker.py index 4b0ce0f..1eacfc9 100644 --- a/cr_checker/tests/test_cr_checker.py +++ b/cr_checker/tests/test_cr_checker.py @@ -432,7 +432,7 @@ def test_has_duplicate_copyright_detects_different_year_ranges(tmp_path): test_file = tmp_path / "file.py" header_template = load_template("py") header1 = header_template.format(year="2026") - header2 = header_template.format(year="2024-2026") + header2 = header_template.format(year="2024-2026" test_file.write_text(header1 + header2 + "some content\n", encoding="utf-8") result = cr_checker.has_duplicate_copyright( From b3d3cc4ac11a0aaeca63d5952cae2ea7a4750104 Mon Sep 17 00:00:00 2001 From: MaximilianSoerenPollak Date: Mon, 20 Jul 2026 11:07:48 +0200 Subject: [PATCH 06/22] fix: remove reuse from pre-commti --- .pre-commit-config.yaml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e6c8436..cac5b4f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,8 +27,3 @@ repos: rev: 21ca5323a9c87ee37a434e0ca908efc0a89daa07 # v0.21.0 hooks: - id: yamlfmt - - repo: https://codeberg.org/fsfe/reuse-tool - rev: a1bb792acda6fd0724936b4ebbdbc8eceb9c0459 # v6.2.0 - hooks: - - id: reuse-lint-file - exclude: \.(svg|tpl|txt|conf|lock|json|in|png)$ From 4977412a4cb782ea8f1dc428e67285c266ef2d5c Mon Sep 17 00:00:00 2001 From: MaximilianSoerenPollak Date: Mon, 20 Jul 2026 11:32:51 +0200 Subject: [PATCH 07/22] fix: PR comments --- .github/workflows/tests.yml | 5 +++++ MODULE.bazel | 5 +---- cr_checker/README.md | 6 +++--- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2ba5e3d..d3594e9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,6 +10,11 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* +# +on: + pull_request: + types: [opened, reopened, synchronize] + integration_tests: runs-on: ubuntu-24.04 permissions: diff --git a/MODULE.bazel b/MODULE.bazel index 5c512d5..193a4e2 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -12,11 +12,8 @@ # ******************************************************************************* module( - name = "score_utils" + name = "score_tools" ) # CR_CHECKER bazel_dep(name = "aspect_rules_py", version = "1.4.0") bazel_dep(name = "score_tooling", version = "1.2.0") -# bazel_dep(name = "rules_python", version = "1.4.1") -# bazel_dep(name = "rules_rust", version = "0.68.1-score") -# bazel_dep(name = "rules_shell", version = "0.5.0") diff --git a/cr_checker/README.md b/cr_checker/README.md index e9d9833..da0883a 100644 --- a/cr_checker/README.md +++ b/cr_checker/README.md @@ -154,12 +154,12 @@ To integrate copyright verification into your Bazel-based project, you can use t #### Usage ```python -load("@score_cr_checker//cr_checker:def.bzl", "copyright_checker") +load("@score_tools//cr_checker:def.bzl", "copyright_checker") copyright_checker( name = "copyright_check", srcs = glob(["src/**/*.cpp", "src/**/*.h"]), - config = "@score_cr_checker//tools/cr_checker/resources:config", - template = "@score_cr_checker//tools/cr_checker/resources:templates", + config = "@score_tools//cr_checker/resources:config", + template = "@score_tools//cr_checker/resources:templates", visibility = ["//visibility:public"], ) ``` From 4c4184764a9288fa2d6215d2d8f9780972a967b1 Mon Sep 17 00:00:00 2001 From: MaximilianSoerenPollak Date: Mon, 20 Jul 2026 12:11:44 +0200 Subject: [PATCH 08/22] chore: linting & formatting --- .github/workflows/tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d3594e9..93cd9d6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,7 +14,6 @@ on: pull_request: types: [opened, reopened, synchronize] - integration_tests: runs-on: ubuntu-24.04 permissions: From a77926cd3325dd9fa4c7241e879ecff6db3f1921 Mon Sep 17 00:00:00 2001 From: MaximilianSoerenPollak Date: Mon, 20 Jul 2026 12:15:00 +0200 Subject: [PATCH 09/22] fix: Remove unused options --- cr_checker/README.md | 8 +++----- cr_checker/cr_checker.bzl | 6 ------ cr_checker/tool/cr_checker.py | 8 -------- cr_checker/tool/pre-commit_wrapper | 2 -- 4 files changed, 3 insertions(+), 21 deletions(-) diff --git a/cr_checker/README.md b/cr_checker/README.md index da0883a..aa9fcb5 100644 --- a/cr_checker/README.md +++ b/cr_checker/README.md @@ -48,7 +48,6 @@ python cr_checker.py -t [options] - **-v**, **--verbose**: Enable debug-level logging. - **-l**, **--log-file**: Path to a log file where logs will be saved. If not provided, logs will print to the console. - **-e**, **--extensions**: List of file extensions to filter, e.g., -e .py .cpp. -- **--use_memory_map**: Use memory-mapped file reading for large files. - **--encoding**: File encoding (default is utf-8). - **--offset**: Additional offset for the header length to account for lines like a shebang. - **--fix**: Setting script into fix mode where copyright header will be added to the files if it's missing from same. @@ -64,9 +63,9 @@ python cr_checker.py -t [options] ```sh python cr_checker.py -t templates.ini -e py cpp -v -l logs.txt my_random_file.cpp my_random_file.py -python cr_checker.py -t templates.ini -e py cpp --offset 24 --use_memory_map @files_to_check.txt +python cr_checker.py -t templates.ini -e py cpp --offset 24 @files_to_check.txt -python cr_checker.py -t templates.ini -e py cpp --fix --offset 24 --use_memory_map @files_to_check.txt +python cr_checker.py -t templates.ini -e py cpp --fix --offset 24 @files_to_check.txt ``` @@ -83,7 +82,7 @@ import os and we use following command: ```sh -python cr_checker.py -t templates.ini -e py cpp --fix --use_memory_map @files_to_check.txt +python cr_checker.py -t templates.ini -e py cpp --fix @files_to_check.txt ``` The result will be following: @@ -175,7 +174,6 @@ copyright_checker( - **offset** (optional): Line offset for checking/modifying files. - **remove_offset** (optional): Number of characters to remove from the beginning of the file. - **debug** (optional): Enables verbose logging for debugging. -- **use_memory_map** (optional): Uses memory-mapped files for performance optimization. - **fix** (optional): Automatically applies fixes instead of just reporting issues. ### Integrate `cr_checker` using Bazel module diff --git a/cr_checker/cr_checker.bzl b/cr_checker/cr_checker.bzl index a23fd9d..a4bc45b 100644 --- a/cr_checker/cr_checker.bzl +++ b/cr_checker/cr_checker.bzl @@ -25,7 +25,6 @@ def copyright_checker( offset = 0, remove_offset = 0, debug = False, - use_memory_map = False, fix = False, target_compatible_with = None): """ @@ -47,8 +46,6 @@ def copyright_checker( Defaults to 0. debug (bool, optional): Whether to enable debug mode, providing additional logs. Defaults to False. - use_memory_map (bool, optional): Whether to use memory mapping for large files to - improve performance. Defaults to False. fix (bool, optional): Whether to apply fixes to files instead of just reporting issues. Defaults to False. @@ -77,9 +74,6 @@ def copyright_checker( if debug: args.append("-v") - if use_memory_map: - args.append("--use_memory_map") - for src in srcs: args.append("$(locations {})".format(src)) diff --git a/cr_checker/tool/cr_checker.py b/cr_checker/tool/cr_checker.py index 0c9545b..f79e5f3 100755 --- a/cr_checker/tool/cr_checker.py +++ b/cr_checker/tool/cr_checker.py @@ -717,13 +717,6 @@ def parse_arguments(argv): help="List of extensions to filter when searching for files, e.g., '.h .cpp'", ) - parser.add_argument( - "--use_memory_map", - action="store_true", - help="Use memory map for reading content of files \ - (should be used reading gigabyte ranged files).", - ) - parser.add_argument( "-f", "--fix", @@ -818,7 +811,6 @@ def main(argv=None): args.fix, exclusion, args.config_file, - args.use_memory_map, args.encoding, args.offset, args.remove_offset, diff --git a/cr_checker/tool/pre-commit_wrapper b/cr_checker/tool/pre-commit_wrapper index 90f1f9d..efeb1de 100755 --- a/cr_checker/tool/pre-commit_wrapper +++ b/cr_checker/tool/pre-commit_wrapper @@ -19,14 +19,12 @@ import os import sys if __name__ == "__main__": - # --template-file and --config-file cannot be set via .pre-commit-config.yaml script_dir = os.path.dirname(os.path.abspath(__file__)) sys.argv.insert(1, '--template-file') sys.argv.insert( 2, os.path.normpath(os.path.join(script_dir, '..', 'resources', 'templates.ini')), ) - sys.argv.insert(3, '--config-file') sys.argv.insert( 4, os.path.normpath(os.path.join(script_dir, '..', 'resources', 'config.json')), From 19585e7c97fa8393455c1030eea662d280060f4f Mon Sep 17 00:00:00 2001 From: MaximilianSoerenPollak Date: Mon, 20 Jul 2026 12:45:32 +0200 Subject: [PATCH 10/22] fix: various fixes --- .pre-commit-config.yaml | 10 +++ .pre-commit-hooks.yaml | 3 +- copyright_exclusions.txt | 1 + cr_checker/resources/BUILD | 20 ----- cr_checker/tool/BUILD | 2 + cr_checker/tool/cr_checker.py | 20 ++--- cr_checker/tool/pre-commit_wrapper | 16 ---- cr_checker/{resources => tool}/templates.ini | 0 templates.ini | 91 ++++++++++++++++++++ 9 files changed, 115 insertions(+), 48 deletions(-) create mode 100644 copyright_exclusions.txt delete mode 100644 cr_checker/resources/BUILD rename cr_checker/{resources => tool}/templates.ini (100%) create mode 100644 templates.ini diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cac5b4f..9d5969c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,3 +27,13 @@ repos: rev: 21ca5323a9c87ee37a434e0ca908efc0a89daa07 # v0.21.0 hooks: - id: yamlfmt + # Ensure every file has a copyright header as per the Eclipse Foundation's requirements + - repo: local + hooks: + - id: copyright + name: Check and fix copyright headers with cr_checker + entry: cr_checker/tool/pre-commit_wrapper --exclusion copyright_exclusions.txt --fix + # this would be the better language implementation, but struggle with python tooling + # language: python + language: script + minimum_pre_commit_version: 3.2.0 diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 3607a34..90fae91 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -12,9 +12,8 @@ # ******************************************************************************* - id: copyright name: Check and fix copyright headers with cr_checker - entry: cr_checker/tool/pre-commit_wrapper --extensions h hpp c cpp rs rst py sh bzl ini yml yaml trlc rsl puml svg BUILD bazel --fix + entry: cr_checker/tool/pre-commit_wrapper --fix # this would be the better language implementation, but struggle with python tooling # language: python language: script minimum_pre_commit_version: 3.2.0 - types_or: [python, yaml, ini, rst, sh, shell, bash, bazel, c, c++, rust, plantuml, svg] diff --git a/copyright_exclusions.txt b/copyright_exclusions.txt new file mode 100644 index 0000000..de38ffc --- /dev/null +++ b/copyright_exclusions.txt @@ -0,0 +1 @@ +cr_checker/tool/templates.ini diff --git a/cr_checker/resources/BUILD b/cr_checker/resources/BUILD deleted file mode 100644 index e11a316..0000000 --- a/cr_checker/resources/BUILD +++ /dev/null @@ -1,20 +0,0 @@ -# ******************************************************************************* -# Copyright (c) 2025 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 -# ******************************************************************************* - -filegroup( - name = "templates", - srcs = [ - "templates.ini", - ], - visibility = ["//visibility:public"], -) diff --git a/cr_checker/tool/BUILD b/cr_checker/tool/BUILD index 8ef719b..192bc8e 100644 --- a/cr_checker/tool/BUILD +++ b/cr_checker/tool/BUILD @@ -12,10 +12,12 @@ # ******************************************************************************* load("@aspect_rules_py//py:defs.bzl", "py_library") + py_library( name = "cr_checker_lib", srcs = [ "cr_checker.py", + "templates.ini", ], visibility = ["//visibility:public"], ) diff --git a/cr_checker/tool/cr_checker.py b/cr_checker/tool/cr_checker.py index f79e5f3..4a9b575 100755 --- a/cr_checker/tool/cr_checker.py +++ b/cr_checker/tool/cr_checker.py @@ -584,6 +584,7 @@ def fix_copyright(path, copyright_text, encoding, offset) -> bool: def process_files( + *, files, templates, fix, @@ -685,7 +686,7 @@ def parse_arguments(argv): "-t", "--template-file", type=Path, - required=True, + default=Path(__file__).parent / "templates.ini", help="Path to the template file", ) @@ -713,7 +714,7 @@ def parse_arguments(argv): "--extensions", type=str, nargs="+", - default=None, + default=["h", "hpp", "c", "cpp", "rs", "rst", "py", "sh", "bzl", "lni", "yml", "yaml", "trlc", "rsl", "puml", "svg", "BUILD", "bazel"], help="List of extensions to filter when searching for files, e.g., '.h .cpp'", ) @@ -806,14 +807,13 @@ def main(argv=None): LOGGER.info("%s!-----------------------!%s", COLORS["RED"], COLORS["ENDC"]) results = process_files( - files, - templates, - args.fix, - exclusion, - args.config_file, - args.encoding, - args.offset, - args.remove_offset, + files=files, + templates=templates, + fix=args.fix, + exclusion=exclusion, + encoding=args.encoding, + offset=args.offset, + remove_offset=args.remove_offset, ) total_no = results["no_copyright"] total_fixes = results["fixed"] diff --git a/cr_checker/tool/pre-commit_wrapper b/cr_checker/tool/pre-commit_wrapper index efeb1de..6b3d028 100755 --- a/cr_checker/tool/pre-commit_wrapper +++ b/cr_checker/tool/pre-commit_wrapper @@ -19,20 +19,4 @@ import os import sys if __name__ == "__main__": - script_dir = os.path.dirname(os.path.abspath(__file__)) - sys.argv.insert(1, '--template-file') - sys.argv.insert( - 2, - os.path.normpath(os.path.join(script_dir, '..', 'resources', 'templates.ini')), - ) - sys.argv.insert( - 4, - os.path.normpath(os.path.join(script_dir, '..', 'resources', 'config.json')), - ) - sys.argv.insert(5, '--exclusion-file') - sys.argv.insert( - 6, - os.path.normpath(os.path.join(script_dir, '..', 'resources', 'exclusion.txt')), - ) - sys.exit(cr_checker.main(sys.argv[1:])) diff --git a/cr_checker/resources/templates.ini b/cr_checker/tool/templates.ini similarity index 100% rename from cr_checker/resources/templates.ini rename to cr_checker/tool/templates.ini diff --git a/templates.ini b/templates.ini new file mode 100644 index 0000000..3bc4f88 --- /dev/null +++ b/templates.ini @@ -0,0 +1,91 @@ +# ******************************************************************************* +# 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 +# ******************************************************************************* +[cpp,c,h,hpp,trlc,rsl] +/******************************************************************************** + * Copyright (c) {year} 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,sh,bzl,ini,yml,yaml,BUILD,bazel] +# ******************************************************************************* +# Copyright (c) {year} 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 +# ******************************************************************************* +[rst] +.. + # ******************************************************************************* + # Copyright (c) {year} 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 + # ******************************************************************************* +[rs] +// ******************************************************************************* +// Copyright (c) {year} 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 +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +[puml] +' ******************************************************************************* +' Copyright (c) {year} 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 +' ******************************************************************************* +[md] + From d5438e551c5d80b86586ee1b5748eb5f61f66cb8 Mon Sep 17 00:00:00 2001 From: MaximilianSoerenPollak Date: Mon, 20 Jul 2026 12:50:32 +0200 Subject: [PATCH 11/22] fix: Add markdown to default extensions --- .pre-commit-config.yaml | 4 +--- .pre-commit-hooks.yaml | 4 +--- cr_checker/tool/cr_checker.py | 2 +- cr_checker/tool/pre-commit_wrapper | 22 ---------------------- 4 files changed, 3 insertions(+), 29 deletions(-) delete mode 100755 cr_checker/tool/pre-commit_wrapper diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9d5969c..c5ec0e7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,8 +32,6 @@ repos: hooks: - id: copyright name: Check and fix copyright headers with cr_checker - entry: cr_checker/tool/pre-commit_wrapper --exclusion copyright_exclusions.txt --fix - # this would be the better language implementation, but struggle with python tooling - # language: python + entry: cr_checker/tool/cr_checker.py --exclusion copyright_exclusions.txt --fix language: script minimum_pre_commit_version: 3.2.0 diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index 90fae91..7612592 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -12,8 +12,6 @@ # ******************************************************************************* - id: copyright name: Check and fix copyright headers with cr_checker - entry: cr_checker/tool/pre-commit_wrapper --fix - # this would be the better language implementation, but struggle with python tooling - # language: python + entry: cr_checker/tool/cr_checker.py --fix language: script minimum_pre_commit_version: 3.2.0 diff --git a/cr_checker/tool/cr_checker.py b/cr_checker/tool/cr_checker.py index 4a9b575..7db98f3 100755 --- a/cr_checker/tool/cr_checker.py +++ b/cr_checker/tool/cr_checker.py @@ -714,7 +714,7 @@ def parse_arguments(argv): "--extensions", type=str, nargs="+", - default=["h", "hpp", "c", "cpp", "rs", "rst", "py", "sh", "bzl", "lni", "yml", "yaml", "trlc", "rsl", "puml", "svg", "BUILD", "bazel"], + default=["md","h", "hpp", "c", "cpp", "rs", "rst", "py", "sh", "bzl", "lni", "yml", "yaml", "trlc", "rsl", "puml", "svg", "BUILD", "bazel"], help="List of extensions to filter when searching for files, e.g., '.h .cpp'", ) diff --git a/cr_checker/tool/pre-commit_wrapper b/cr_checker/tool/pre-commit_wrapper deleted file mode 100755 index 6b3d028..0000000 --- a/cr_checker/tool/pre-commit_wrapper +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 - -# ******************************************************************************* -# 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 checking if artifacts have proper copyright.""" - -import cr_checker -import os -import sys - -if __name__ == "__main__": - sys.exit(cr_checker.main(sys.argv[1:])) From 2abbd0f47b1f1b0a0b200fea9f1dcadbfeb56e47 Mon Sep 17 00:00:00 2001 From: MaximilianSoerenPollak Date: Mon, 20 Jul 2026 12:50:37 +0200 Subject: [PATCH 12/22] chore: fix copyright --- README.md | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 74212f6..c2002dd 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,16 @@ - -SPDX-License-Identifier: Apache-2.0 -******************************************************************************* ---> # Tools From 66a472db33ad350a26f920051c5c7d0b440ebd7c Mon Sep 17 00:00:00 2001 From: MaximilianSoerenPollak Date: Mon, 20 Jul 2026 13:58:24 +0200 Subject: [PATCH 13/22] fix: Simplify inputs Now takes all inputs always so pre-commit & bazel behave the same --- BUILD | 7 ++ cr_checker/cr_checker.bzl | 22 +++-- cr_checker/tool/cr_checker.py | 157 ++++++++++------------------------ 3 files changed, 61 insertions(+), 125 deletions(-) diff --git a/BUILD b/BUILD index ca5de74..2aebf5c 100644 --- a/BUILD +++ b/BUILD @@ -10,3 +10,10 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* +load("@score_tools//cr_checker:cr_checker.bzl", "copyright_checker") + + +copyright_checker( + name = "copyright", + visibility = ["//visibility:public"], +) diff --git a/cr_checker/cr_checker.bzl b/cr_checker/cr_checker.bzl index a4bc45b..ef2e2cd 100644 --- a/cr_checker/cr_checker.bzl +++ b/cr_checker/cr_checker.bzl @@ -17,9 +17,8 @@ load("@aspect_rules_py//py:defs.bzl", "py_binary") def copyright_checker( name, - srcs, visibility, - template, + template = None, exclusion = None, extensions = [], offset = 0, @@ -56,10 +55,11 @@ def copyright_checker( "{}.check".format(name), "{}.fix".format(name), ] - - args = [ - "-t $(location {})".format(template), - ] + args = [] + if template: + args.append( + "-t $(location {})".format(template), + ) if len(extensions): args.append("-e {exts}".format( exts = " ".join([exts for exts in extensions]), @@ -74,15 +74,13 @@ def copyright_checker( if debug: args.append("-v") - for src in srcs: - args.append("$(locations {})".format(src)) - + data = [] for t_name in t_names: if t_name == "{}.fix".format(name): args.insert(0, "--fix") - data = srcs[:] - data.append(template) + if template: + data.append(template) if exclusion: data.append(exclusion) @@ -90,7 +88,7 @@ def copyright_checker( name = t_name, main = "cr_checker.py", srcs = [ - "@score_tooling//cr_checker/tool:cr_checker_lib", + "@score_tools//cr_checker/tool:cr_checker_lib", ], args = args, data = data, diff --git a/cr_checker/tool/cr_checker.py b/cr_checker/tool/cr_checker.py index 7db98f3..e0b675d 100755 --- a/cr_checker/tool/cr_checker.py +++ b/cr_checker/tool/cr_checker.py @@ -21,6 +21,7 @@ import os import re import shutil +import subprocess import sys import tempfile from datetime import datetime @@ -77,37 +78,6 @@ def format(self, record): return super().format(record) -class ParamFileAction(argparse.Action): # pylint: disable=too-few-public-methods - """ - A custom argparse action to support exclusive parameter files for command-line arguments. - - The `ParamFileAction` class allows users to specify a parameter file (prefixed with '@') - containing file paths or other inputs, which will override any additional inputs provided - in the command line. If a parameter file is found, its contents are used exclusively, - and all other inputs are ignored. If no parameter file is provided, standard inputs are used. - - Attributes: - parser (argparse.ArgumentParser): The argument parser instance. - namespace (argparse.Namespace): The namespace where arguments are stored. - values (list): The list of argument values passed from the command line. - option_string (str, optional): The option string that triggered this action, if any. - - Methods: - __call__(parser, namespace, values, option_string=None): Processes the arguments. - - If any value starts with '@', it reads the parameter file and sets `file_paths` - in `namespace`. - - If no parameter file is detected, it directly assigns `values` to `namespace`. - """ - - def __call__(self, parser, namespace, values, option_string=None): - paramfile = next((v[1:] for v in values if v.startswith("@")), None) - if paramfile: - with open(paramfile, "r", encoding="utf-8") as handle: - file_paths = [line.strip() for line in handle if line.strip()] - setattr(namespace, self.dest, file_paths) - else: - setattr(namespace, self.dest, values) - def convert_bre_to_regex(template: str) -> str: """ @@ -146,7 +116,7 @@ def line_to_flexible_regex(line: str) -> str: return "".join(result) -def load_templates(path): +def load_templates(path: Path): """ Loads the copyright templates from a configuration file. @@ -158,13 +128,15 @@ def load_templates(path): and the value is the template string from the config. """ - def add_template_for_extensions(templates: dict, extensions: list, template: str): + def add_template_for_extensions( + templates: dict[str, str], extensions: list[str], template: str + ): # Remove trailing lines from template and ensure line end template = template.rstrip() + "\n" for extension in extensions: templates[extension] = template - templates = {} + templates: dict[str, str] = {} current_extensions = [] with open(path, "r", encoding="utf-8") as file: @@ -463,7 +435,7 @@ def get_files_from_dir(directory, exts=None): return collected_files -def collect_inputs(inputs, exts=None): +def collect_inputs(inputs: list[str], exts=None): """ Collects files from a list of input paths, optionally filtering by file extensions. @@ -483,8 +455,15 @@ def collect_inputs(inputs, exts=None): """ all_files = [] LOGGER.debug("Extensions: %s", exts) + workspace_dir = Path(os.environ.get("BUILD_WORKSPACE_DIRECTORY", "").strip()) + if not inputs: + cmds = ["git", "ls-files", "--cached", "--other", "--exclude-standard"] + found_dirs = subprocess.run( + cmds, capture_output=True, text=True, cwd=workspace_dir, check=True + ) + inputs = found_dirs.stdout.splitlines() for i in inputs: - item = Path(i) + item = Path(workspace_dir / i) if item.is_dir(): LOGGER.debug("Processing directory: %s", item) all_files.extend(get_files_from_dir(item, exts)) @@ -591,8 +570,6 @@ def process_files( exclusion=[], use_mmap=False, encoding="utf-8", - offset=0, - remove_offset=0, ): # pylint: disable=too-many-arguments """ Processes a list of files to check for the presence of copyright text. @@ -607,11 +584,6 @@ def process_files( use_mmap (bool): Flag for using mmap function for reading files (instead of standard option). encoding (str): Encoding type to use when reading the file. - offset (int): Additional number of characters to read beyond the length - of `copyright_text`, used to account for extra content - (such as a shebang) before the copyright text. - remove_offset(int): Flag for removing old header from source files - (before applying the new one) in number of chars. Returns: int: The number of files that do not contain the required copyright text. @@ -636,30 +608,28 @@ def process_files( # Automatically detect shebang and use its offset if no manual offset provided shebang_offset = detect_shebang_offset(item, encoding) - effective_offset = offset + shebang_offset if offset == 0 else offset if has_duplicate_copyright( - item, templates[key], use_mmap, encoding, effective_offset + item, templates[key], use_mmap, encoding, shebang_offset ): LOGGER.error("Duplicate copyright header in: %s", item) results["duplicate_copyright"] += 1 elif not has_copyright( - item, templates[key], use_mmap, encoding, effective_offset + item, templates[key], use_mmap, encoding, shebang_offset ): - if has_any_copyright(item, use_mmap, encoding, effective_offset): + if has_any_copyright(item, use_mmap, encoding, shebang_offset): LOGGER.warning( "Wrong copyright format in: %s, expected format from template", item ) elif fix: - if remove_offset: - remove_old_header(item, encoding, remove_offset) fix_result = fix_copyright( - item, templates[key], encoding, effective_offset + item, templates[key], encoding, shebang_offset ) results["no_copyright"] += 1 if fix_result: results["fixed"] += 1 else: + # TODO: Clean up error message file name LOGGER.error( "Missing copyright header in: %s, use --fix to introduce it", item ) @@ -686,6 +656,7 @@ def parse_arguments(argv): "-t", "--template-file", type=Path, + required=False, default=Path(__file__).parent / "templates.ini", help="Path to the template file", ) @@ -714,7 +685,27 @@ def parse_arguments(argv): "--extensions", type=str, nargs="+", - default=["md","h", "hpp", "c", "cpp", "rs", "rst", "py", "sh", "bzl", "lni", "yml", "yaml", "trlc", "rsl", "puml", "svg", "BUILD", "bazel"], + default=[ + "md", + "h", + "hpp", + "c", + "cpp", + "rs", + "rst", + "py", + "sh", + "bzl", + "lni", + "yml", + "yaml", + "trlc", + "rsl", + "puml", + "svg", + "BUILD", + "bazel", + ], help="List of extensions to filter when searching for files, e.g., '.h .cpp'", ) @@ -729,27 +720,10 @@ def parse_arguments(argv): "--encoding", default="utf-8", help="File encoding (default: utf-8)." ) - parser.add_argument( - "--offset", - dest="offset", - type=int, - default=0, - help="Additional length offset to account for characters like a shebang (default is 0)", - ) - - parser.add_argument( - "--remove-offset", - dest="remove_offset", - type=int, - default=0, - help="Offset to remove old header from beginning of the file \ - (supported only with --fix mode)", - ) - parser.add_argument( "inputs", - nargs="+", - action=ParamFileAction, + nargs="*", + default=None, help="Directories and/or files to parse.", ) @@ -798,59 +772,16 @@ def main(argv=None): LOGGER.debug("Running check on files: %s", files) - if args.fix and args.remove_offset: - LOGGER.info("%s!------DANGER ZONE------!%s", COLORS["RED"], COLORS["ENDC"]) - LOGGER.info("Remove offset set! This can REMOVE parts of source files!") - LOGGER.info( - "Use ONLY if invalid copyright header is present that needs to be removed!" - ) - LOGGER.info("%s!-----------------------!%s", COLORS["RED"], COLORS["ENDC"]) - results = process_files( files=files, templates=templates, fix=args.fix, exclusion=exclusion, encoding=args.encoding, - offset=args.offset, - remove_offset=args.remove_offset, ) total_no = results["no_copyright"] - total_fixes = results["fixed"] total_duplicates = results["duplicate_copyright"] - LOGGER.info("=" * 64) - LOGGER.info("Process completed.") - LOGGER.info( - "Total files without copyright: %s%d%s", - COLORS["RED"] if total_no > 0 else COLORS["GREEN"], - total_no, - COLORS["ENDC"], - ) - LOGGER.info( - "Total files with duplicate copyright: %s%d%s", - COLORS["RED"] if total_duplicates > 0 else COLORS["GREEN"], - total_duplicates, - COLORS["ENDC"], - ) - if not exclusion_valid: - LOGGER.info("The exclusion file contains paths that do not exist.") - if args.fix: - total_not_fixed = total_no - total_fixes - LOGGER.info( - "Total files that were fixed: %s%d%s", - COLORS["GREEN"], - total_fixes, - COLORS["ENDC"], - ) - LOGGER.info( - "Total files that were NOT fixed: %s%d%s", - COLORS["RED"] if total_not_fixed > 0 else COLORS["GREEN"], - total_not_fixed, - COLORS["ENDC"], - ) - LOGGER.info("=" * 64) - return 0 if (total_no == 0 and total_duplicates == 0 and exclusion_valid) else 1 From f2cf10b06a3a865b5a9959fbe7a0c74e48e5004c Mon Sep 17 00:00:00 2001 From: MaximilianSoerenPollak Date: Mon, 20 Jul 2026 13:59:05 +0200 Subject: [PATCH 14/22] chore: Formatting --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c2002dd..6dcaae5 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ ----------------------------------------------------------------------------- --> + # Tools ## Why this repository exists From 48f5491e239f462f0f681ae494be7dae4f568b56 Mon Sep 17 00:00:00 2001 From: MaximilianSoerenPollak Date: Mon, 20 Jul 2026 16:29:39 +0200 Subject: [PATCH 15/22] chore: remove no longer needed files --- cr_checker/tests/.bazelrc | 6 ------ cr_checker/tests/.bazelversion | 1 - 2 files changed, 7 deletions(-) delete mode 100644 cr_checker/tests/.bazelrc delete mode 120000 cr_checker/tests/.bazelversion diff --git a/cr_checker/tests/.bazelrc b/cr_checker/tests/.bazelrc deleted file mode 100644 index b425658..0000000 --- a/cr_checker/tests/.bazelrc +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright (c) 2026 Contributors to the Eclipse Foundation -# -# SPDX-License-Identifier: Apache-2.0 - -common --registry=https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/ -common --registry=https://bcr.bazel.build diff --git a/cr_checker/tests/.bazelversion b/cr_checker/tests/.bazelversion deleted file mode 120000 index 96cf949..0000000 --- a/cr_checker/tests/.bazelversion +++ /dev/null @@ -1 +0,0 @@ -../../.bazelversion \ No newline at end of file From 4653184606d9ec9aad4d6eb00c8853bf083986e0 Mon Sep 17 00:00:00 2001 From: MaximilianSoerenPollak Date: Mon, 20 Jul 2026 16:46:28 +0200 Subject: [PATCH 16/22] chore: adapt README --- cr_checker/README.md | 198 ++++++++++++++++--------------------------- 1 file changed, 74 insertions(+), 124 deletions(-) diff --git a/cr_checker/README.md b/cr_checker/README.md index aa9fcb5..0293758 100644 --- a/cr_checker/README.md +++ b/cr_checker/README.md @@ -18,99 +18,34 @@ ## Features - Checks files for specified copyright headers based on file extensions. +- When no explicit inputs are given, automatically discovers files via `git ls-files --cached --other --exclude-standard`, so it always operates on the full set of tracked and untracked-but-not-ignored files in the repository. - Configurable logging, including color-coded output for easy visibility of log levels. -- Supports parameter files for flexible input handling. - Can use memory mapping for large file handling. -- Customizable file encoding and offset adjustments for header text positioning. +- Customizable file encoding. +- Automatically detects and skips past a shebang (`#!...`) line before looking for the header. - Can append copyright headers. -- Can remove provided number of characters from beginning of the file. ## Requirements -- Python 3.6+ -- `argparse`, `logging`, `os`, `sys`, `mmap`, `tempfile`, and `pathlib` (standard library modules) - -## Installation - -To use `cr_checker.py`, simply clone this repository. +- Python 3.12+ +- `argparse`, `logging`, `os`, `sys`, `mmap`, `subprocess`, `tempfile`, and `pathlib` (standard library modules) +- `git` available on `PATH` (used to discover files when no explicit inputs are given) ## Usage -The script can be run from the command line with various options to customize its behavior: - -```bash -python cr_checker.py -t [options] -``` +`cr_checker` is **not intended to be invoked directly**. It's meant to be wired into your module through one of the two supported integrations — [pre-commit](#how-to-pre-commit) or [Bazel](#how-to-bazel) — both described below under [Integrating `cr_checker` into your own module](#integrating-cr_checker-into-your-own-module). Both integrations run the exact same script; the flags below are simply the ones you pass through `args:` in your `pre-commit` hook config or through the matching parameters of the `copyright_checker` Bazel macro. ### Arguments -- **-t**, **--template-file**: (Required) Path to the template file that defines the copyright text for each file extension. +- **-t**, **--template-file**: Path to the template file that defines the copyright text for each file extension. Defaults to the bundled `templates.ini` next to the script. - **-v**, **--verbose**: Enable debug-level logging. - **-l**, **--log-file**: Path to a log file where logs will be saved. If not provided, logs will print to the console. -- **-e**, **--extensions**: List of file extensions to filter, e.g., -e .py .cpp. +- **-e**, **--extensions**: List of file extensions to filter, e.g., `-e py cpp`. This list replaces (rather than extends) the built-in default list. - **--encoding**: File encoding (default is utf-8). -- **--offset**: Additional offset for the header length to account for lines like a shebang. +- **--exclusion-file**: Path to a file listing paths (one per line, relative to the repository root) to exclude from the check. - **--fix**: Setting script into fix mode where copyright header will be added to the files if it's missing from same. -- **--remove-offset**: Number of characters to remove before appending proper copyright header (works only with `--fix` option). -- **inputs**: (Required) Directories or files to parse, or a parameter file prefixed with @ that lists files or directories. - -> NOTE: Option `--remove-offset` can have severe consequences if the offset is miscalculated. Use with **extreme caution**. - -> NOTE: Setting directory as `.` will cause that tool removes your complete workspace! This is connected with how Bazel includes python into build. **DO NOT USE THIS OPTION UNLESS YOU'RE 100% SURE IN WHAT YOU'RE DOING**. - -### Examples - -```sh -python cr_checker.py -t templates.ini -e py cpp -v -l logs.txt my_random_file.cpp my_random_file.py - -python cr_checker.py -t templates.ini -e py cpp --offset 24 @files_to_check.txt - -python cr_checker.py -t templates.ini -e py cpp --fix --offset 24 @files_to_check.txt - -``` - -#### A bit more about `--offset` - -This mode is special that will enable tool to do advance search & replace copyright headers. For example, assuming that we have following python implementation: - -```python -#!/usr/bin/env python3 - -import os -``` - -and we use following command: - -```sh -python cr_checker.py -t templates.ini -e py cpp --fix @files_to_check.txt -``` - -The result will be following: - -```python -################## -# COPYRIGHT HEADER -################## -#!/usr/bin/env python3 - -import os -``` - -This is of course not what we want, and for that we use `--offset=24` where 24 is number of char + 1 (for new line char). -if we apply this arguments now on same file, the outcome is different. +- **inputs**: (Optional) Directories and/or files to check. Neither integration passes these explicitly for a full-repo check: `pre-commit` passes whichever files it decided to run against instead, and the Bazel macro never passes any, which makes the tool fall back to running `git ls-files --cached --other --exclude-standard` (resolved against `BUILD_WORKSPACE_DIRECTORY` if set, otherwise the current working directory) and checking everything that comes back. -```python -#!/usr/bin/env python3 - -################## -# COPYRIGHT HEADER -################## - -import os -``` - -On another hand, `--offset` has also another role. Assuming that a header text in file is soo big that copyright header is in the middle of the file, with regular command, the tool will not detect copyright headed. With `--offset=` we can tell the tool from where to start considering the search for -copyright header. ### Template File Format @@ -130,68 +65,83 @@ Example templates.ini: ## Exit Codes - 0: All files contain the required copyright text. -- 1: Some files are missing the required copyright text. +- 1: Some files are missing the required copyright text, have a duplicate/malformed header, or the exclusion file contains invalid entries. - Other: Error encountered during file processing. -### Logging and Color-Coded Output -By default, logs are printed to the console in color-coded format to indicate log levels. You can redirect logs to a file using the -l option. +## Integrating `cr_checker` into your own module -#### Log Colors +There are two supported ways to run `cr_checker` against your own repository: as a `pre-commit` hook, or as a Bazel target. Both rely on the same default behavior described above: if you don't tell it which files to look at, it discovers them itself. -- DEBUG: Blue -- INFO: Green -- WARNING: Yellow -- ERROR: Red +### How-to: pre-commit -## Bazel integration +This repository ships a [`.pre-commit-hooks.yaml`](../.pre-commit-hooks.yaml) that defines a `copyright` hook (`language: script`, running `cr_checker.py --fix`), so any repository can pull it in as a remote `pre-commit` hook. -### Copyright Checker Bazel Macro +1. In your module's `.pre-commit-config.yaml`, add this repository as a hook source: -To integrate copyright verification into your Bazel-based project, you can use the `copyright_checker` macro. This macro allows you to check source files for compliance with a specified copyright template and configuration. Additionally, it can automatically apply fixes when necessary. + ```yaml + repos: + - repo: https://github.com/eclipse-score/tools + rev: # pin to a specific commit/tag of this repo + hooks: + - id: copyright + args: + # - --exclusion-file=copyright_exclusions.txt # optional + # - --template-file=templates.ini # optional, provide your own header templates + # - --extensions=py cpp h # optional, overrides the built-in list + ``` -#### Usage +2. Any `args` you add here are appended after the hard-coded `--fix` from the hook definition, and the hook runs with your repository as the working directory, so relative paths (like `copyright_exclusions.txt` above) are resolved against your repo root. -```python -load("@score_tools//cr_checker:def.bzl", "copyright_checker") -copyright_checker( - name = "copyright_check", - srcs = glob(["src/**/*.cpp", "src/**/*.h"]), - config = "@score_tools//cr_checker/resources:config", - template = "@score_tools//cr_checker/resources:templates", - visibility = ["//visibility:public"], -) -``` +3. `pre-commit` itself decides which files to pass to the hook (the changed/staged files by default, or every tracked file with `pre-commit run --all-files`), so in normal `pre-commit` usage `cr_checker` checks exactly the files `pre-commit` hands it — the "no inputs → scan the whole repo via `git ls-files`" fallback described above mainly matters when you invoke the script directly or via Bazel (see below) without passing any files. -#### Parameters +4. Run `pre-commit run --all-files` once after adding the hook to confirm your repo is clean, then let it run on every commit as usual. -- **name**: Unique identifier for the rule. -- **srcs**: List of source files to check. -- **visibility**: Defines which targets can access this rule. -- **template**: Path to the copyright header template. -- **config**: Path to the project-specific configuration. -- **extensions** (optional): List of file extensions to filter files. Defaults to all files. -- **offset** (optional): Line offset for checking/modifying files. -- **remove_offset** (optional): Number of characters to remove from the beginning of the file. -- **debug** (optional): Enables verbose logging for debugging. -- **fix** (optional): Automatically applies fixes instead of just reporting issues. +### How-to: Bazel -### Integrate `cr_checker` using Bazel module +1. Declare a dependency on this repository's Bazel module in your `MODULE.bazel`: -The CopyRight check tool is integrated in other bazel repository using Bazel modules mechanism. The current tool is not registered within BCR so private Bazel registry needs to be select. To select custom Bazel registry add following lines into .bazelrc: + ```python + bazel_dep(name = "score_tools", version = "") + ``` -```python -common --registry=https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/ -common --registry=https://bcr.bazel.build -``` + If this module isn't available from the Bazel Central Registry yet, add the registry that hosts it to your `.bazelrc` first, e.g.: -This will allow Bazel to look into project Bazel registry. After that all what is needed is to add following lines in MODULE.bazel: + ```python + common --registry=https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/ + common --registry=https://bcr.bazel.build + ``` -```python -############################################################################### -# -# CopyRight checker dependencies -# -############################################################################### -bazel_dep(name = "score_cr_checker", version = "0.2.2") -``` +2. In the `BUILD` file where you want the check available (typically your repo root), load and instantiate the `copyright_checker` macro: + + ```python + load("@score_tools//cr_checker:cr_checker.bzl", "copyright_checker") + + copyright_checker( + name = "copyright_check", + # template: optional, path to your own templates.ini; omit to use the bundled default templates. + # template = "//:templates.ini", + # exclusion = "//:copyright_exclusions.txt", # optional, list of files to ignore + # extensions = ["py", "cpp", "h"], # optional, overrides the built-in list + visibility = ["//visibility:public"], + ) + ``` + + This defines two runnable targets plus convenience aliases: + - `bazel test //:copyright_check.check` (aliased as `bazel run //:copyright-check`) — reports missing/duplicate headers. + - `bazel run //:copyright_check.fix` (aliased as `bazel run //:copyright-fix`) — inserts missing headers. + +3. There is no `srcs` parameter anymore — the macro doesn't take a file list at all. Every invocation shells out to `git ls-files --cached --other --exclude-standard` against your workspace and filters the result by `extensions`, so it always covers the whole repository (minus whatever `exclusion` lists). + +4. Because of that, **use `bazel run`, not `bazel build`/`bazel test`**: the tool relies on the `BUILD_WORKSPACE_DIRECTORY` environment variable to find your real workspace root, and Bazel only sets that variable for `run`. It also needs `git` available on `PATH` at run time. + +#### Parameters + +- **name**: Unique identifier for the rule. +- **visibility**: Defines which targets can access this rule. +- **template** (optional): Path to the copyright header template. Defaults to the tool's bundled `templates.ini` if omitted. +- **exclusion** (optional): Path to the project-specific exclusion file. +- **extensions** (optional): List of file extensions to filter files. Defaults to the tool's built-in list. +- **debug** (optional): Enables verbose logging for debugging. +- **fix** (optional): Automatically applies fixes instead of just reporting issues. +- **target_compatible_with** (optional): Standard Bazel platform-compatibility constraint list. From 59b895fbff28e2ac4946b5f8b0d89b72a492bb9a Mon Sep 17 00:00:00 2001 From: MaximilianSoerenPollak Date: Mon, 20 Jul 2026 17:14:18 +0200 Subject: [PATCH 17/22] fix: fix test files --- cr_checker/tests/test_cr_checker.py | 102 +++++++++++----------------- 1 file changed, 39 insertions(+), 63 deletions(-) diff --git a/cr_checker/tests/test_cr_checker.py b/cr_checker/tests/test_cr_checker.py index 1eacfc9..265a60e 100644 --- a/cr_checker/tests/test_cr_checker.py +++ b/cr_checker/tests/test_cr_checker.py @@ -36,7 +36,7 @@ def load_cr_checker_module(): # load the license template def load_template(extension: str) -> str: cr_checker = load_cr_checker_module() - template_file = Path(__file__).resolve().parents[1] / "resources" / "templates.ini" + template_file = Path(__file__).resolve().parents[1] / "tool" / "templates.ini" templates = cr_checker.load_templates(template_file) return templates[extension] @@ -122,13 +122,11 @@ def test_process_files_detects_header(prepare_test_with_header): test_file, extension, header_template = prepare_test_with_header results = cr_checker.process_files( - [test_file], - {extension: header_template}, - False, + files=[test_file], + templates={extension: header_template}, + fix=False, use_mmap=False, encoding="utf-8", - offset=0, - remove_offset=0, ) assert results["no_copyright"] == 0 @@ -139,13 +137,11 @@ def test_process_files_detects_missing_header(prepare_test_no_header): test_file, extension, header_template, tmp_path = prepare_test_no_header results = cr_checker.process_files( - [test_file], - {extension: header_template}, - False, + files=[test_file], + templates={extension: header_template}, + fix=False, use_mmap=False, encoding="utf-8", - offset=0, - remove_offset=0, ) assert results["no_copyright"] == 1 @@ -156,13 +152,11 @@ def test_process_files_inserts_missing_header(prepare_test_no_header): test_file, extension, header_template, tmp_path = prepare_test_no_header results = cr_checker.process_files( - [test_file], - {extension: header_template}, - True, + files=[test_file], + templates={extension: header_template}, + fix=True, use_mmap=False, encoding="utf-8", - offset=0, - remove_offset=0, ) assert results["no_copyright"] == 1 @@ -176,14 +170,12 @@ def test_process_files_skips_exclusion_with_missing_header(prepare_test_no_heade test_file, extension, header_template, tmp_path = prepare_test_no_header results = cr_checker.process_files( - [test_file], - {extension: header_template}, - False, - [str(test_file)], + files=[test_file], + templates={extension: header_template}, + fix=False, + exclusion=[str(test_file)], use_mmap=False, encoding="utf-8", - offset=0, - remove_offset=0, ) assert results["no_copyright"] == 0 @@ -202,13 +194,11 @@ def test_process_files_accepts_header_after_shebang(tmp_path): ) results = cr_checker.process_files( - [script], - {"py": header_template}, - False, + files=[script], + templates={"py": header_template}, + fix=False, use_mmap=False, encoding="utf-8", - offset=0, - remove_offset=0, ) assert results["no_copyright"] == 0 @@ -226,13 +216,11 @@ def test_process_files_fix_inserts_header_after_shebang(tmp_path): current_year = datetime.now().year results = cr_checker.process_files( - [script], - {"py": header_template}, - True, + files=[script], + templates={"py": header_template}, + fix=True, use_mmap=False, encoding="utf-8", - offset=0, - remove_offset=0, ) assert results["fixed"] == 1 @@ -253,13 +241,11 @@ def test_process_files_accepts_header_without_shebang(tmp_path): script.write_text(header + "print('hi')\n", encoding="utf-8") results = cr_checker.process_files( - [script], - {"py": header_template}, - False, + files=[script], + templates={"py": header_template}, + fix=False, use_mmap=False, encoding="utf-8", - offset=0, - remove_offset=0, ) assert results["no_copyright"] == 0 @@ -274,13 +260,11 @@ def test_process_files_fix_inserts_header_without_shebang(tmp_path): current_year = datetime.now().year results = cr_checker.process_files( - [script], - {"py": header_template}, - True, + files=[script], + templates={"py": header_template}, + fix=True, use_mmap=False, encoding="utf-8", - offset=0, - remove_offset=0, ) assert results["fixed"] == 1 @@ -315,13 +299,11 @@ def test_process_files_accepts_flexible_border(tmp_path): header_template = load_template("cpp") results = cr_checker.process_files( - [test_file], - {"cpp": header_template}, - False, + files=[test_file], + templates={"cpp": header_template}, + fix=False, use_mmap=False, encoding="utf-8", - offset=0, - remove_offset=0, ) assert results["no_copyright"] == 0 @@ -337,13 +319,11 @@ def test_process_files_accepts_header_with_trailing_blank_line(tmp_path): test_file.write_text(header + "\nsome content\n", encoding="utf-8") results = cr_checker.process_files( - [test_file], - {"py": header_template}, - False, + files=[test_file], + templates={"py": header_template}, + fix=False, use_mmap=False, encoding="utf-8", - offset=0, - remove_offset=0, ) assert results["no_copyright"] == 0 @@ -358,13 +338,11 @@ def test_process_files_fix_inserts_trailing_blank_line(tmp_path): current_year = datetime.now().year cr_checker.process_files( - [test_file], - {"py": header_template}, - True, + files=[test_file], + templates={"py": header_template}, + fix=True, use_mmap=False, encoding="utf-8", - offset=0, - remove_offset=0, ) expected_header = header_template.format(year=current_year) @@ -413,13 +391,11 @@ def test_process_files_detects_duplicate_header(tmp_path): test_file.write_text(header + header + "some content\n", encoding="utf-8") results = cr_checker.process_files( - [test_file], - {"py": header_template}, - False, + files=[test_file], + templates={"py": header_template}, + fix=False, use_mmap=False, encoding="utf-8", - offset=0, - remove_offset=0, ) assert results["duplicate_copyright"] == 1 @@ -432,7 +408,7 @@ def test_has_duplicate_copyright_detects_different_year_ranges(tmp_path): test_file = tmp_path / "file.py" header_template = load_template("py") header1 = header_template.format(year="2026") - header2 = header_template.format(year="2024-2026" + header2 = header_template.format(year="2024-2026") test_file.write_text(header1 + header2 + "some content\n", encoding="utf-8") result = cr_checker.has_duplicate_copyright( From 07c8dbe53fcc060263163f0f640e2ae7052e8c10 Mon Sep 17 00:00:00 2001 From: MaximilianSoerenPollak Date: Mon, 20 Jul 2026 21:55:13 +0200 Subject: [PATCH 18/22] fix: fix PR findings --- .github/workflows/tests.yml | 45 ++++++++--------- README.md | 1 + cr_checker/cr_checker.bzl | 8 --- cr_checker/deps.bzl | 37 -------------- cr_checker/tool/BUILD | 1 - cr_checker/tool/cr_checker.py | 11 +++-- templates.ini | 91 ----------------------------------- 7 files changed, 30 insertions(+), 164 deletions(-) delete mode 100644 cr_checker/deps.bzl delete mode 100644 templates.ini diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 93cd9d6..def574c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,25 +14,26 @@ on: pull_request: types: [opened, reopened, synchronize] -integration_tests: - runs-on: ubuntu-24.04 - permissions: - contents: read - actions: write - steps: - - name: Checkout repository - uses: actions/checkout@v7.0.0 - - uses: castler/setup-bazel@cache-optimized - with: - bazelisk-cache: true - disk-cache: integration_tests - repository-cache: true - cache-optimized: true - cache-save: ${{ github.ref == 'refs/heads/main' }} - # - name: Run python_basics integration tests - # run: | - # cd python_basics/integration_tests - # bazel test //... - - name: Run cr_checker unit tests - run: | - uv run pytest cr_checker/tests/ +jobs: + integration_tests: + runs-on: ubuntu-24.04 + permissions: + contents: read + actions: write + steps: + - name: Checkout repository + uses: actions/checkout@v7.0.0 + - uses: castler/setup-bazel@cache-optimized + with: + bazelisk-cache: true + disk-cache: integration_tests + repository-cache: true + cache-optimized: true + cache-save: ${{ github.ref == 'refs/heads/main' }} + # - name: Run python_basics integration tests + # run: | + # cd python_basics/integration_tests + # bazel test //... + - name: Run cr_checker unit tests + run: | + uv run pytest cr_checker/tests/ diff --git a/README.md b/README.md index 6dcaae5..818a361 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ + # Tools ## Why this repository exists diff --git a/cr_checker/cr_checker.bzl b/cr_checker/cr_checker.bzl index ef2e2cd..272c900 100644 --- a/cr_checker/cr_checker.bzl +++ b/cr_checker/cr_checker.bzl @@ -21,8 +21,6 @@ def copyright_checker( template = None, exclusion = None, extensions = [], - offset = 0, - remove_offset = 0, debug = False, fix = False, target_compatible_with = None): @@ -32,7 +30,6 @@ def copyright_checker( Args: name (str): The name of the rule, used as an identifier in the build system. - srcs (list): A list of source file paths to check. visibility (list): A list defining the visibility of the rule, specifying which targets can use this rule. template (str, optional): Path to the template resource used for validation. @@ -41,8 +38,6 @@ def copyright_checker( File format: one path per line, relative to the repository root. extensions (list, optional): A list of file extensions to filter the source files. Defaults to an empty list, meaning all files are checked. - offset (int, optional): The line offset for applying checks or modifications. - Defaults to 0. debug (bool, optional): Whether to enable debug mode, providing additional logs. Defaults to False. fix (bool, optional): Whether to apply fixes to files instead of just reporting issues. @@ -68,9 +63,6 @@ def copyright_checker( if exclusion: args.append("--exclusion-file $(location {})".format(exclusion)) - if offset: - args.append("--offset {}".format(offset)) - if debug: args.append("-v") diff --git a/cr_checker/deps.bzl b/cr_checker/deps.bzl deleted file mode 100644 index bf84784..0000000 --- a/cr_checker/deps.bzl +++ /dev/null @@ -1,37 +0,0 @@ -# ******************************************************************************* -# 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("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") - -def score_cr_checker_deps(): - if not native.existing_rule("rules_cc"): - http_archive( - name = "rules_cc", - urls = ["https://github.com/bazelbuild/rules_cc/releases/download/0.1.1/rules_cc-0.1.1.tar.gz"], - sha256 = "712d77868b3152dd618c4d64faaddefcc5965f90f5de6e6dd1d5ddcd0be82d42", - strip_prefix = "rules_cc-0.1.1", - ) - if not native.existing_rule("aspect_rules_py"): - http_archive( - name = "rules_python", - sha256 = "9f9f3b300a9264e4c77999312ce663be5dee9a56e361a1f6fe7ec60e1beef9a3", - strip_prefix = "rules_python-1.4.1", - url = "https://github.com/bazel-contrib/rules_python/releases/download/1.4.1/rules_python-1.4.1.tar.gz", - ) - if not native.existing_rule("aspect_rules_py"): - http_archive( - name = "aspect_rules_py", - sha256 = "8e9a1f00e4ba5696f9e93a770a6c1de863544cce489df91809fc3a4027ccfddc", - strip_prefix = "rules_py-1.4.0", - url = "https://github.com/aspect-build/rules_py/releases/download/v1.4.0/rules_py-v1.4.0.tar.gz", - ) diff --git a/cr_checker/tool/BUILD b/cr_checker/tool/BUILD index 192bc8e..0ba1546 100644 --- a/cr_checker/tool/BUILD +++ b/cr_checker/tool/BUILD @@ -12,7 +12,6 @@ # ******************************************************************************* load("@aspect_rules_py//py:defs.bzl", "py_library") - py_library( name = "cr_checker_lib", srcs = [ diff --git a/cr_checker/tool/cr_checker.py b/cr_checker/tool/cr_checker.py index e0b675d..0dc11ba 100755 --- a/cr_checker/tool/cr_checker.py +++ b/cr_checker/tool/cr_checker.py @@ -15,7 +15,6 @@ """The tool for checking if artifacts have proper copyright.""" import argparse -import json import logging import mmap import os @@ -182,9 +181,9 @@ def load_exclusion(path): exclusion = [] valid = True with open(path, "r", encoding="utf-8") as file: - exclusion = file.read().splitlines() - - for item in exclusion: + exclusion_raw = file.read().splitlines() + exclusion = exclusion_raw + for item in exclusion_raw: path = Path(item) if not path.exists(): LOGGER.error("Excluded file %s does not exist.", item) @@ -567,7 +566,7 @@ def process_files( files, templates, fix, - exclusion=[], + exclusion:list[str]|None =None, use_mmap=False, encoding="utf-8", ): # pylint: disable=too-many-arguments @@ -588,6 +587,8 @@ def process_files( Returns: int: The number of files that do not contain the required copyright text. """ + if exclusion is None: + exclusion = [] results = {"no_copyright": 0, "fixed": 0, "duplicate_copyright": 0} for item in files: name = Path(item).name diff --git a/templates.ini b/templates.ini deleted file mode 100644 index 3bc4f88..0000000 --- a/templates.ini +++ /dev/null @@ -1,91 +0,0 @@ -# ******************************************************************************* -# 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 -# ******************************************************************************* -[cpp,c,h,hpp,trlc,rsl] -/******************************************************************************** - * Copyright (c) {year} 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,sh,bzl,ini,yml,yaml,BUILD,bazel] -# ******************************************************************************* -# Copyright (c) {year} 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 -# ******************************************************************************* -[rst] -.. - # ******************************************************************************* - # Copyright (c) {year} 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 - # ******************************************************************************* -[rs] -// ******************************************************************************* -// Copyright (c) {year} 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 -// -// -// SPDX-License-Identifier: Apache-2.0 -// ******************************************************************************* -[puml] -' ******************************************************************************* -' Copyright (c) {year} 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 -' ******************************************************************************* -[md] - From ec36c5b23d852b0335c78cdf5f79ec7f45e3087b Mon Sep 17 00:00:00 2001 From: MaximilianSoerenPollak Date: Mon, 20 Jul 2026 21:58:04 +0200 Subject: [PATCH 19/22] fix: fix test workflow --- .github/workflows/tests.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index def574c..ac62650 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,7 +15,7 @@ on: pull_request: types: [opened, reopened, synchronize] jobs: - integration_tests: + tests: runs-on: ubuntu-24.04 permissions: contents: read @@ -30,6 +30,8 @@ jobs: repository-cache: true cache-optimized: true cache-save: ${{ github.ref == 'refs/heads/main' }} + - name: ⚙️ Setup uv + uses: astral-sh/setup-uv@v7 # - name: Run python_basics integration tests # run: | # cd python_basics/integration_tests From 6e4236e290b4ec19a25521b9b42f78d4a64f9564 Mon Sep 17 00:00:00 2001 From: MaximilianSoerenPollak Date: Mon, 20 Jul 2026 22:12:39 +0200 Subject: [PATCH 20/22] fix: remove not needed LICENCE --- cr_checker/LICENSE | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 cr_checker/LICENSE diff --git a/cr_checker/LICENSE b/cr_checker/LICENSE deleted file mode 100644 index 8c69a8b..0000000 --- a/cr_checker/LICENSE +++ /dev/null @@ -1,13 +0,0 @@ -Copyright 2025 Contributors to the Eclipse Foundation - -Licensed 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. From caf036cae1a4126e8ba5a436d28f82b90d07dd88 Mon Sep 17 00:00:00 2001 From: MaximilianSoerenPollak Date: Mon, 20 Jul 2026 22:20:09 +0200 Subject: [PATCH 21/22] fix: fix Pr comments --- MODULE.bazel | 2 +- cr_checker/cr_checker.bzl | 8 ++++---- cr_checker/tests/test_cr_checker.py | 1 - cr_checker/tool/BUILD | 3 +++ cr_checker/tool/cr_checker.py | 10 +++------- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 193a4e2..5b1de9c 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -15,5 +15,5 @@ module( name = "score_tools" ) # CR_CHECKER -bazel_dep(name = "aspect_rules_py", version = "1.4.0") +bazel_dep(name = "aspect_rules_py", version = "1.6.3") bazel_dep(name = "score_tooling", version = "1.2.0") diff --git a/cr_checker/cr_checker.bzl b/cr_checker/cr_checker.bzl index 272c900..39fc2c4 100644 --- a/cr_checker/cr_checker.bzl +++ b/cr_checker/cr_checker.bzl @@ -67,14 +67,14 @@ def copyright_checker( args.append("-v") data = [] + if template: + data.append(template) + if exclusion: + data.append(exclusion) for t_name in t_names: if t_name == "{}.fix".format(name): args.insert(0, "--fix") - if template: - data.append(template) - if exclusion: - data.append(exclusion) py_binary( name = t_name, diff --git a/cr_checker/tests/test_cr_checker.py b/cr_checker/tests/test_cr_checker.py index 265a60e..1d5fad9 100644 --- a/cr_checker/tests/test_cr_checker.py +++ b/cr_checker/tests/test_cr_checker.py @@ -15,7 +15,6 @@ from __future__ import annotations import importlib.util -import json import pytest from datetime import datetime from pathlib import Path diff --git a/cr_checker/tool/BUILD b/cr_checker/tool/BUILD index 0ba1546..6261e30 100644 --- a/cr_checker/tool/BUILD +++ b/cr_checker/tool/BUILD @@ -12,6 +12,9 @@ # ******************************************************************************* load("@aspect_rules_py//py:defs.bzl", "py_library") +# `templates.ini` is inside srcs. +# This is because if it would be in the `data` attr we would need to find the actual path of it with a lot of effort +# So this is a good workaround that does not change the workings of the lib. py_library( name = "cr_checker_lib", srcs = [ diff --git a/cr_checker/tool/cr_checker.py b/cr_checker/tool/cr_checker.py index 0dc11ba..2a7ffcf 100755 --- a/cr_checker/tool/cr_checker.py +++ b/cr_checker/tool/cr_checker.py @@ -77,7 +77,6 @@ def format(self, record): return super().format(record) - def convert_bre_to_regex(template: str) -> str: """ Convert BRE-style template (literal by default) to standard regex. @@ -181,20 +180,17 @@ def load_exclusion(path): exclusion = [] valid = True with open(path, "r", encoding="utf-8") as file: - exclusion_raw = file.read().splitlines() - exclusion = exclusion_raw - for item in exclusion_raw: + for item in file.read().splitlines(): path = Path(item) if not path.exists(): LOGGER.error("Excluded file %s does not exist.", item) - exclusion.remove(item) valid = False continue if not path.is_file(): - exclusion.remove(item) LOGGER.error("Excluded file %s is not a file.", item) valid = False continue + exclusion.append(item) LOGGER.debug(exclusion) return exclusion, valid @@ -566,7 +562,7 @@ def process_files( files, templates, fix, - exclusion:list[str]|None =None, + exclusion: list[str] | None = None, use_mmap=False, encoding="utf-8", ): # pylint: disable=too-many-arguments From 9c2fd4103b7f4a04300edb43f395e2ccf6514678 Mon Sep 17 00:00:00 2001 From: MaximilianSoerenPollak Date: Tue, 21 Jul 2026 10:24:27 +0200 Subject: [PATCH 22/22] chore: linting & formatting --- cr_checker/tool/BUILD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cr_checker/tool/BUILD b/cr_checker/tool/BUILD index 6261e30..66c370b 100644 --- a/cr_checker/tool/BUILD +++ b/cr_checker/tool/BUILD @@ -12,7 +12,7 @@ # ******************************************************************************* load("@aspect_rules_py//py:defs.bzl", "py_library") -# `templates.ini` is inside srcs. +# `templates.ini` is inside srcs. # This is because if it would be in the `data` attr we would need to find the actual path of it with a lot of effort # So this is a good workaround that does not change the workings of the lib. py_library(