[libc] Write LF line endings from cmake_format.py on every platform - #222742
Conversation
process_file() opened the temporary file in text mode with the default newline handling, so the "\n" endings produced by format_cmake_content() were substituted with the platform separator on write. On Windows that turns every line of an LF CMake file into CRLF, and since .gitattributes declares no eol rules and GettingStarted.md tells contributors to clone with core.autocrlf=false, the rewritten endings are what gets committed. Formatting one file that way shows up as a diff on all of its lines. Reading already normalizes CRLF to LF through universal newlines, so pass newline="\n" on the write and configure stdout the same way, which covers the piped output and the --diff text as well. Both new tests compare the bytes on disk (and on stdout) with what format_cmake_content() returned; they fail on Windows before this change and are no-ops on platforms whose separator is already LF.
|
Hello @Dev-next-gen 👋 Thank you for submitting a Pull Request (PR) to the LLVM Project. Since this is your first PR, here are a few useful links covering our main contribution policies and review practices.
Please reply to this message to confirm that you have read these policies, especially the LLVM AI Tool Use Policy, and that any AI tool usage has been noted in the PR description. Frequently asked questionsHow do I add reviewers? This PR will be automatically labeled, and the relevant teams will be notified. For some parts of the project, reviewers may also be added automatically. You can also add reviewers manually using the Reviewers section on this page. If you cannot use that section, it is probably because you do not have write permissions for the repository. In that case, you can request a review by tagging reviewers in a comment using What if there are no comments? If you have not received any comments on your PR after a week, you can request a review by pinging the PR with a comment such as “Ping”. The common courtesy ping rate is once a week. Please remember that you are asking for volunteer time from other developers. Are any special GitHub settings required to contribute to LLVM? We only require contributors to have a public email address associated with their GitHub commits, see this section of LLVM Developer Policy for details. If you have questions, feel free to leave a comment on this PR, or ask on LLVM Discord or LLVM Discourse. Thank you, |
|
@llvm/pr-subscribers-libc Author: Leo Camus (Dev-next-gen) ChangesI ran the libc CMake formatter on Windows and it gave back a file whose every line had changed:
The read side already normalizes: The same substitution applies to stdout, which affects the piped output and the One behaviour change worth flagging: on Windows, a CMake file that currently has CRLF endings and needs reformatting now comes out with LF rather than CRLF. Files that are already formatted are not written at all, before or after, since the comparison is made on the LF-normalized read. For the tests, everything in I did not have AI tools used Full diff: https://github.com/llvm/llvm-project/pull/222742.diff 2 Files Affected:
diff --git a/libc/utils/cmake_format.py b/libc/utils/cmake_format.py
index 74d5f82e79c99..9087bc40ce487 100755
--- a/libc/utils/cmake_format.py
+++ b/libc/utils/cmake_format.py
@@ -1377,7 +1377,10 @@ def process_file(
temp_file = target_path.with_name(
f".{target_path.name}.tmp_{os.getpid()}"
)
- with open(temp_file, "w", encoding="utf-8") as f:
+ # newline="\n" disables the newline substitution a text-mode
+ # write does: format_cmake_content() emits "\n" endings and the
+ # file has to land on disk with those.
+ with open(temp_file, "w", encoding="utf-8", newline="\n") as f:
f.write(formatted)
os.replace(temp_file, target_path)
print(f"Formatted {filepath}")
@@ -1422,6 +1425,10 @@ def find_cmake_files(paths: list[str]) -> list[str]:
def main() -> None:
"""Entry point: parses CLI arguments and drives file discovery, pre-scanning, and formatting."""
+ # Formatted output and diffs go to stdout verbatim, for the same reason
+ # process_file() writes files with newline="\n".
+ sys.stdout.reconfigure(newline="\n")
+
parser = argparse.ArgumentParser(
description="LLVM and LLVM-libc CMake Formatter Utility",
formatter_class=argparse.RawDescriptionHelpFormatter,
diff --git a/libc/utils/cmake_format_test.py b/libc/utils/cmake_format_test.py
index 6381ac877b243..b699f1e5da9b2 100644
--- a/libc/utils/cmake_format_test.py
+++ b/libc/utils/cmake_format_test.py
@@ -503,5 +503,48 @@ def test_bug_directive_comment_preservation(self):
self.assertEqual(cmake_format.format_cmake_content(code), expected)
+class TestFileWriting(unittest.TestCase):
+ """Regression tests for the bytes that land on disk, not the formatted string."""
+
+ def test_inplace_write_keeps_lf_endings(self):
+ """In-place formatting writes exactly the bytes format_cmake_content produced.
+
+ A text-mode write substitutes the platform line separator, which
+ rewrites every line of an LF file as CRLF on Windows.
+ """
+ import contextlib
+ import io
+ import tempfile
+
+ code = "add_library(foo STATIC\n a.c\n)\n"
+ expected = cmake_format.format_cmake_content(code).encode("utf-8")
+
+ with tempfile.TemporaryDirectory() as tmpdir:
+ path = os.path.join(tmpdir, "CMakeLists.txt")
+ with open(path, "w", encoding="utf-8", newline="") as f:
+ f.write(code)
+
+ with contextlib.redirect_stdout(io.StringIO()):
+ self.assertTrue(cmake_format.process_file(path, inplace=True))
+
+ with open(path, "rb") as f:
+ self.assertEqual(f.read(), expected)
+
+ def test_stdout_write_keeps_lf_endings(self):
+ """Formatting through stdin/stdout does not rewrite the line endings either."""
+ import subprocess
+
+ code = "add_library(foo STATIC\n a.c\n)\n"
+ expected = cmake_format.format_cmake_content(code).encode("utf-8")
+
+ proc = subprocess.run(
+ [sys.executable, os.path.join(SCRIPT_DIR, "cmake_format.py")],
+ input=code.encode("utf-8"),
+ stdout=subprocess.PIPE,
+ check=True,
+ )
+ self.assertEqual(proc.stdout, expected)
+
+
if __name__ == "__main__":
unittest.main()
|
michaelrj-google
left a comment
There was a problem hiding this comment.
this is a good catch, thanks for fixing it!
CC @kaladron
|
@Dev-next-gen Congratulations on having your first Pull Request (PR) merged into the LLVM Project! Your changes will be combined with recent changes from other authors, then tested by our build bots. If there is a problem with a build, you may receive a report in an email or a comment on this PR. Please check whether problems have been caused by your change specifically, as the builds can include changes from many authors. It is not uncommon for your change to be included in a build that fails due to someone else's changes, or infrastructure issues. How to do this, and the rest of the post-merge process, is covered in detail here. If your change does cause a problem, it may be reverted, or you can revert it yourself. This is a normal part of LLVM development. You can fix your changes and open a new PR to merge them again. If you don't get any reports, no action is required from you. Your changes are working as expected, well done! |
…lvm#222742) I ran the libc CMake formatter on Windows and it gave back a file whose every line had changed: ``` $ printf 'add_library(foo STATIC\n a.c\n)\n' > CMakeLists.txt $ python libc/utils/cmake_format.py -i CMakeLists.txt Formatted CMakeLists.txt $ python -c "print(open('CMakeLists.txt','rb').read())" b'add_library(foo STATIC\r\n a.c\r\n)\r\n' ``` `format_cmake_content()` joins its lines with `"\n"`, but `process_file()` hands that string to `open(temp_file, "w", encoding="utf-8")`, and a text-mode write substitutes the platform line separator. So on Windows, reformatting one command in a file rewrites the endings of the whole file. `.gitattributes` declares no `text`/`eol` rules and `llvm/docs/GettingStarted.md` tells people to clone with `core.autocrlf=false`, so nothing normalizes those endings back before the commit. The read side already normalizes: `open(..., "r")` gives universal newlines, so a CRLF file is LF by the time the formatter sees it. Passing `newline="\n"` on the write makes both ends agree and gives the same bytes on every platform. The same substitution applies to stdout, which affects the piped output and the `--diff` text (a CRLF diff does not apply against an LF file), so I configured stdout the same way in `main()`. One behaviour change worth flagging: on Windows, a CMake file that currently has CRLF endings and needs reformatting now comes out with LF rather than CRLF. Files that are already formatted are not written at all, before or after, since the comparison is made on the LF-normalized read. For the tests, everything in `cmake_format_test.py` today asserts on the string returned by `format_cmake_content()`, so nothing covers what lands on disk. I added two cases that compare the bytes on disk and on stdout against that string. They fail on Windows before this change (the assertion prints `b'...\r\n...' != b'...\n...'`) and pass after; on Linux and macOS they pass either way, since the separator is already LF. The file went from 47 to 49 tests, all passing under `python -m unittest cmake_format_test`. I did not have `black` available on that machine, so the added code follows the surrounding style by hand rather than by running the formatter. AI tools used
…lvm#222742) I ran the libc CMake formatter on Windows and it gave back a file whose every line had changed: ``` $ printf 'add_library(foo STATIC\n a.c\n)\n' > CMakeLists.txt $ python libc/utils/cmake_format.py -i CMakeLists.txt Formatted CMakeLists.txt $ python -c "print(open('CMakeLists.txt','rb').read())" b'add_library(foo STATIC\r\n a.c\r\n)\r\n' ``` `format_cmake_content()` joins its lines with `"\n"`, but `process_file()` hands that string to `open(temp_file, "w", encoding="utf-8")`, and a text-mode write substitutes the platform line separator. So on Windows, reformatting one command in a file rewrites the endings of the whole file. `.gitattributes` declares no `text`/`eol` rules and `llvm/docs/GettingStarted.md` tells people to clone with `core.autocrlf=false`, so nothing normalizes those endings back before the commit. The read side already normalizes: `open(..., "r")` gives universal newlines, so a CRLF file is LF by the time the formatter sees it. Passing `newline="\n"` on the write makes both ends agree and gives the same bytes on every platform. The same substitution applies to stdout, which affects the piped output and the `--diff` text (a CRLF diff does not apply against an LF file), so I configured stdout the same way in `main()`. One behaviour change worth flagging: on Windows, a CMake file that currently has CRLF endings and needs reformatting now comes out with LF rather than CRLF. Files that are already formatted are not written at all, before or after, since the comparison is made on the LF-normalized read. For the tests, everything in `cmake_format_test.py` today asserts on the string returned by `format_cmake_content()`, so nothing covers what lands on disk. I added two cases that compare the bytes on disk and on stdout against that string. They fail on Windows before this change (the assertion prints `b'...\r\n...' != b'...\n...'`) and pass after; on Linux and macOS they pass either way, since the separator is already LF. The file went from 47 to 49 tests, all passing under `python -m unittest cmake_format_test`. I did not have `black` available on that machine, so the added code follows the surrounding style by hand rather than by running the formatter. AI tools used
…lvm#222742) I ran the libc CMake formatter on Windows and it gave back a file whose every line had changed: ``` $ printf 'add_library(foo STATIC\n a.c\n)\n' > CMakeLists.txt $ python libc/utils/cmake_format.py -i CMakeLists.txt Formatted CMakeLists.txt $ python -c "print(open('CMakeLists.txt','rb').read())" b'add_library(foo STATIC\r\n a.c\r\n)\r\n' ``` `format_cmake_content()` joins its lines with `"\n"`, but `process_file()` hands that string to `open(temp_file, "w", encoding="utf-8")`, and a text-mode write substitutes the platform line separator. So on Windows, reformatting one command in a file rewrites the endings of the whole file. `.gitattributes` declares no `text`/`eol` rules and `llvm/docs/GettingStarted.md` tells people to clone with `core.autocrlf=false`, so nothing normalizes those endings back before the commit. The read side already normalizes: `open(..., "r")` gives universal newlines, so a CRLF file is LF by the time the formatter sees it. Passing `newline="\n"` on the write makes both ends agree and gives the same bytes on every platform. The same substitution applies to stdout, which affects the piped output and the `--diff` text (a CRLF diff does not apply against an LF file), so I configured stdout the same way in `main()`. One behaviour change worth flagging: on Windows, a CMake file that currently has CRLF endings and needs reformatting now comes out with LF rather than CRLF. Files that are already formatted are not written at all, before or after, since the comparison is made on the LF-normalized read. For the tests, everything in `cmake_format_test.py` today asserts on the string returned by `format_cmake_content()`, so nothing covers what lands on disk. I added two cases that compare the bytes on disk and on stdout against that string. They fail on Windows before this change (the assertion prints `b'...\r\n...' != b'...\n...'`) and pass after; on Linux and macOS they pass either way, since the separator is already LF. The file went from 47 to 49 tests, all passing under `python -m unittest cmake_format_test`. I did not have `black` available on that machine, so the added code follows the surrounding style by hand rather than by running the formatter. AI tools used
I ran the libc CMake formatter on Windows and it gave back a file whose every line had changed:
format_cmake_content()joins its lines with"\n", butprocess_file()hands that string toopen(temp_file, "w", encoding="utf-8"), and a text-mode write substitutes the platform line separator. So on Windows, reformatting one command in a file rewrites the endings of the whole file..gitattributesdeclares notext/eolrules andllvm/docs/GettingStarted.mdtells people to clone withcore.autocrlf=false, so nothing normalizes those endings back before the commit.The read side already normalizes:
open(..., "r")gives universal newlines, so a CRLF file is LF by the time the formatter sees it. Passingnewline="\n"on the write makes both ends agree and gives the same bytes on every platform.The same substitution applies to stdout, which affects the piped output and the
--difftext (a CRLF diff does not apply against an LF file), so I configured stdout the same way inmain().One behaviour change worth flagging: on Windows, a CMake file that currently has CRLF endings and needs reformatting now comes out with LF rather than CRLF. Files that are already formatted are not written at all, before or after, since the comparison is made on the LF-normalized read.
For the tests, everything in
cmake_format_test.pytoday asserts on the string returned byformat_cmake_content(), so nothing covers what lands on disk. I added two cases that compare the bytes on disk and on stdout against that string. They fail on Windows before this change (the assertion printsb'...\r\n...' != b'...\n...') and pass after; on Linux and macOS they pass either way, since the separator is already LF. The file went from 47 to 49 tests, all passing underpython -m unittest cmake_format_test.I did not have
blackavailable on that machine, so the added code follows the surrounding style by hand rather than by running the formatter.AI tools used