-
Notifications
You must be signed in to change notification settings - Fork 341
DAOS-623 build: add a linter to normalize config #17781
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
c079815
27bd41c
8b5a47a
5fffdf3
47535c4
952401c
dcca903
bd00143
29e4603
96222e8
7f9ef6f
6802dfd
7029d44
d780627
a83411b
6b6d647
639c26f
06ab20d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,31 +1,28 @@ | ||
| [component] | ||
| component=daos | ||
|
|
||
| [commit_versions] | ||
| argobots=v1.2 | ||
| fused=v1.0.0 | ||
| pmdk=2.1.3 | ||
| isal=v2.31.1 | ||
| isal_crypto=v2.25.0 | ||
| spdk=v24.09 | ||
| ofi=v1.22.0 | ||
| mercury=v2.4.1 | ||
| ofi=v1.22.0 | ||
| pmdk=2.1.3 | ||
| protobufc=v1.3.3 | ||
| spdk=v24.09 | ||
| ucx=v1.14.1 | ||
|
|
||
| [repos] | ||
| argobots=https://github.com/pmodels/argobots.git | ||
| fused=https://github.com/daos-stack/fused.git | ||
| pmdk=https://github.com/daos-stack/pmdk.git | ||
| isal=https://github.com/intel/isa-l.git | ||
| isal_crypto=https://github.com/intel/isa-l_crypto.git | ||
| spdk=https://github.com/spdk/spdk.git | ||
| ofi=https://github.com/ofiwg/libfabric.git | ||
| mercury=https://github.com/mercury-hpc/mercury.git | ||
| ofi=https://github.com/ofiwg/libfabric.git | ||
| pmdk=https://github.com/daos-stack/pmdk.git | ||
| protobufc=https://github.com/protobuf-c/protobuf-c.git | ||
| spdk=https://github.com/spdk/spdk.git | ||
| ucx=https://github.com/openucx/ucx.git | ||
|
|
||
| [patch_versions] | ||
| spdk=0001_3428322b812fe31cc3e1d0308a7f5bd4b06b9886.diff,0002_spdk_rwf_nowait.patch,0003_external_isal.patch | ||
| mercury=0001_dep_versions.patch,0002_ofi_counters.patch,0003_ofi_auth_key.patch | ||
| argobots=0001_411e5b344642ebc82190fd8b125db512e5b449d1.diff,0002_bb0c908abfac4bfe37852eee621930634183c6aa.diff | ||
| mercury=0001_dep_versions.patch,0002_ofi_counters.patch,0003_ofi_auth_key.patch | ||
| spdk=0001_3428322b812fe31cc3e1d0308a7f5bd4b06b9886.diff,0002_spdk_rwf_nowait.patch,0003_external_isal.patch | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,234 @@ | ||
| #!/usr/bin/env python3 | ||
| # | ||
| # Copyright 2026 Google LLC | ||
| # | ||
| # SPDX-License-Identifier: BSD-2-Clause-Patent | ||
| # | ||
| # Runs lint and auto-fix for utils/build.config for the DAOS project. | ||
|
|
||
| """Git hook script to check and fix the format of utils/build.config.""" | ||
|
|
||
| import os | ||
| import subprocess # nosec B404 | ||
| import sys | ||
| import unittest | ||
|
|
||
|
|
||
| def print_githook_header(name): | ||
| """Print the standard git hook header.""" | ||
| print(f"{name + ':':<17} ", end='', flush=True) | ||
|
|
||
|
|
||
| def git_diff_cached_files(file_path): | ||
| """Check if the file has staged changes.""" | ||
| target = os.environ.get('TARGET') | ||
| if not target: | ||
| # If TARGET is not set, we can't easily determine what to diff against | ||
| # in the same way hook_base.sh does. For now, we'll return True to | ||
| # force a check if we're not sure, or try to find a base. | ||
| return True | ||
|
|
||
| cmd = ['git', 'diff', target, '--cached', '--name-only', '--diff-filter=d', '--', file_path] | ||
| result = subprocess.run( | ||
| cmd, capture_output=True, text=True, check=False, | ||
| shell=False) # nosec B603 | ||
| return result.stdout.strip() | ||
|
Comment on lines
+17
to
+35
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it not work in the env to call the bash equivalents for these functions?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, I'm not sure which functions you are referring to here. Maybe commented on a different line than you wanted?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mean instead of writing
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The short answer is the bash function is defined in hook_base.sh and not visible to python. And it's a one-liner. Maybe what we should do is create a python library to do what the bash scripts do and then we could invoke python from bash :-)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Have you tried calling it from python? It does seem in my env at least that it works. I defined this bash script And this python And it seems to work
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, for |
||
|
|
||
|
|
||
| def process_build_config(lines): | ||
| """Parse, sort, and normalize the build config lines.""" | ||
| global_lines = [] | ||
| sections = [] | ||
| current_section = None | ||
|
|
||
| for line in lines: | ||
| stripped = line.strip() | ||
| if stripped.startswith('[') and stripped.endswith(']'): | ||
| new_section = stripped[1:-1] | ||
| current_section = {'name': new_section, 'header': line, 'lines': []} | ||
| sections.append(current_section) | ||
| elif current_section is None: | ||
| global_lines.append(line) | ||
| else: | ||
| current_section['lines'].append(line) | ||
|
|
||
| def get_units(lines): | ||
| units = [] | ||
| current_comments = [] | ||
| for line in lines: | ||
| stripped = line.strip() | ||
| if not stripped: | ||
| continue | ||
| if stripped.startswith(('#', ';')): | ||
| current_comments.append(line) | ||
| elif '=' in stripped: | ||
| key, val = line.split('=', 1) | ||
| units.append({ | ||
| 'comments': current_comments, | ||
| 'key': key.strip().lower(), | ||
| 'val': val.strip() | ||
| }) | ||
| current_comments = [] | ||
| else: | ||
| current_comments.append(line) | ||
| if current_comments: | ||
| units.append({ | ||
| 'comments': current_comments, | ||
| 'key': None, | ||
| 'val': None | ||
| }) | ||
| return units | ||
|
|
||
| def format_unit(unit): | ||
| res = [] | ||
| res.extend(unit['comments']) | ||
| if unit['key'] is not None: | ||
| res.append(f"{unit['key']}={unit['val']}\n") | ||
| return res | ||
|
|
||
| def format_section(sec): | ||
| res = [] | ||
| res.append(sec['header']) | ||
| units = get_units(sec['lines']) | ||
| key_units = sorted([u for u in units if u['key'] is not None], | ||
| key=lambda x: x['key']) | ||
| other_units = [u for u in units if u['key'] is None] | ||
| for u in key_units: | ||
| res.extend(format_unit(u)) | ||
| for u in other_units: | ||
| res.extend(format_unit(u)) | ||
| return res | ||
|
|
||
| new_content = [] | ||
| global_filtered = [line for line in global_lines if line.strip()] | ||
| new_content.extend(global_filtered) | ||
|
|
||
| for i, sec in enumerate(sections): | ||
| if i > 0: | ||
| new_content.append("\n") | ||
| new_content.extend(format_section(sec)) | ||
|
|
||
| return new_content | ||
|
|
||
|
|
||
| def run_tests(): | ||
| """Run unit tests for the functional bits.""" | ||
| class TestBuildConfig(unittest.TestCase): | ||
| """Unit tests for build config processing.""" | ||
|
|
||
| def test_sorting(self): | ||
| """Test that sections are sorted.""" | ||
| lines = [ | ||
| "[section]\n", | ||
| "b=2\n", | ||
| "a=1\n" | ||
| ] | ||
| expected = [ | ||
| "[section]\n", | ||
| "a=1\n", | ||
| "b=2\n" | ||
| ] | ||
| self.assertEqual(process_build_config(lines), expected) | ||
|
|
||
| def test_casing(self): | ||
| """Test that keys are lowercase.""" | ||
| lines = [ | ||
| "[section]\n", | ||
| "Key=Val\n" | ||
| ] | ||
| expected = [ | ||
| "[section]\n", | ||
| "key=Val\n" | ||
| ] | ||
| self.assertEqual(process_build_config(lines), expected) | ||
|
|
||
| def test_comments_and_empty_lines(self): | ||
| """Test that comments stay with following lines and empty lines are removed.""" | ||
| lines = [ | ||
| "[section]\n", | ||
| "b=2\n", | ||
| "\n", | ||
| "# comment\n", | ||
| "a=1\n" | ||
| ] | ||
| expected = [ | ||
| "[section]\n", | ||
| "# comment\n", | ||
| "a=1\n", | ||
| "b=2\n" | ||
| ] | ||
| self.assertEqual(process_build_config(lines), expected) | ||
|
|
||
| def test_section_spacing(self): | ||
| """Test that sections are separated by exactly one empty line.""" | ||
| lines = [ | ||
| "[sec1]\n", | ||
| "a=1\n", | ||
| "[sec2]\n", | ||
| "b=2\n" | ||
| ] | ||
| expected = [ | ||
| "[sec1]\n", | ||
| "a=1\n", | ||
| "\n", | ||
| "[sec2]\n", | ||
| "b=2\n" | ||
| ] | ||
| self.assertEqual(process_build_config(lines), expected) | ||
|
|
||
| def test_no_empty_lines_at_ends(self): | ||
| """Test that no empty lines are at start or end of file.""" | ||
| lines = [ | ||
| "\n", | ||
| "[sec1]\n", | ||
| "a=1\n", | ||
| "\n" | ||
| ] | ||
| expected = [ | ||
| "[sec1]\n", | ||
| "a=1\n" | ||
| ] | ||
| self.assertEqual(process_build_config(lines), expected) | ||
|
|
||
| suite = unittest.TestLoader().loadTestsFromTestCase(TestBuildConfig) | ||
| result = unittest.TextTestRunner(verbosity=2).run(suite) | ||
| sys.exit(0 if result.wasSuccessful() else 1) | ||
|
|
||
|
|
||
| def main(): | ||
| """Main entry point for the git hook.""" | ||
| if len(sys.argv) > 1 and sys.argv[1] == '--test': | ||
| run_tests() | ||
|
|
||
| print_githook_header("Build Config") | ||
|
|
||
| filename = "utils/build.config" | ||
|
|
||
| if not git_diff_cached_files(filename): | ||
| print(f"No changes to {filename}. Skipping") | ||
| sys.exit(0) | ||
|
|
||
| print(f"Checking and fixing {filename} for sorting and casing") | ||
|
|
||
| if not os.path.exists(filename): | ||
| sys.exit(0) | ||
|
|
||
| with open(filename, 'r', encoding='utf-8') as file_handle: | ||
| lines = file_handle.readlines() | ||
|
|
||
| new_content = process_build_config(lines) | ||
|
|
||
| if lines != new_content: | ||
| with open(filename, 'w', encoding='utf-8') as file_handle: | ||
| file_handle.writelines(new_content) | ||
| msg = (f" ERROR: {filename} was not correctly formatted. " | ||
| "Auto-fixes applied.") | ||
| print(msg, file=sys.stderr) | ||
| msg = (f" Please review with 'git diff {filename}' and " | ||
| "re-stage the changes.") | ||
| print(msg, file=sys.stderr) | ||
| sys.exit(1) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is holdover from when scons_local supported cart, daos, cppr, and iof independently