Repository: llvm/llvm-project

Issue:
[Support] Querying the terminal width on Linux is broken
The Linux implementation of `Process::StandardOutColumns()`/`Process::StandardErrColumns()` (or, really, the underlying implementation of `getColumns()` in `Support/Unix/Process.inc`) seems wrong to me and doesn’t work on my system (Fedora 42) at least: both functions always return `0`. From looking at the commit history around those functions, we used to query this via `ioctl()` with `TIOCGWINSZ`, but according to a3eb3d3d, this was ‘essentially disabled’ in https://reviews.llvm.org/D61326, and a3eb3d3d then removed the functionality entirely. Now, we rely exclusively on the ‘environment variable’ `COLUMNS` for this.

The problem with this is that `COLUMNS` is apparently not really an ‘environment variable’ but rather a shell variable, i.e. while accessible inside the shell (e.g. `echo $COLUMNS` works), it is not exported to any child processes spawned by that shell (at least not consistently on all systems). This is the case for instance on my system (Fedora 42) and I also tested it on a Debian 12 server; in both cases, running `std::getenv("COLUMNS")` always returns `nullptr`, for every combination of shell+terminal available to me.

As an aside, while a3eb3d3d also mentions that ‘Nobody has complained for one year’ wrt us relying on `COLUMNS` for this, there is exactly 1 combined use of these functions in the entire monorepo (and no tests for that matter but this also seems hard to test): the Clang driver calls `StandardErrColumns()` to try and determine a sensible error message length if `-fmessage-length` isn’t passed, which as a result is also very noticeably broken on my system. Interestingly, LLDB on Linux actually just uses `ioctl()` to figure out the terminal width and doesn’t bother calling either of these helpers.

I think we should undo these changes on some capacity and fall back on `ioctl()` if it is available and if `COLUMNS` is not defined. https://reviews.llvm.org/D61326 mentions that this functionality was disabled ‘for POSIX compatibility’, but I think we should still be able to make use of this on *some* systems (termios.h is part of the POSIX standard, and afaik `sys/ioctl.h` is always available on Linux at least). That said, I’m not an export on platform-dependent stuff like this so maybe there are issues with that, but the fact remains that these functions are just broken on some systems...

CC @MaskRay, @Endilll because iirc you’re familiar with this topic and @hubert-reinterpretcast because you reviewed D61326, but admittedly that was 6 years ago so I wouldn’t be surprised if you don’t remember much about this ;Þ



Discussion:

Patch:
diff --git a/llvm/cmake/config-ix.cmake b/llvm/cmake/config-ix.cmake
index 9895469973e47..0fcd73e752311 100644
--- a/llvm/cmake/config-ix.cmake
+++ b/llvm/cmake/config-ix.cmake
@@ -19,6 +19,7 @@ if (ANDROID OR CYGWIN OR CMAKE_SYSTEM_NAME MATCHES "AIX|DragonFly|FreeBSD|Haiku|
   set(HAVE_SYS_MMAN_H 1)
   set(HAVE_SYSEXITS_H 1)
   set(HAVE_UNISTD_H 1)
+  set(HAVE_SYS_IOCTL_H 1)
 elseif (APPLE)
   set(HAVE_MACH_MACH_H 1)
   set(HAVE_MALLOC_MALLOC_H 1)
@@ -26,6 +27,7 @@ elseif (APPLE)
   set(HAVE_SYS_MMAN_H 1)
   set(HAVE_SYSEXITS_H 1)
   set(HAVE_UNISTD_H 1)
+  set(HAVE_SYS_IOCTL_H 1)
 elseif (WIN32)
   set(HAVE_MACH_MACH_H 0)
   set(HAVE_MALLOC_MALLOC_H 0)
@@ -33,6 +35,7 @@ elseif (WIN32)
   set(HAVE_SYS_MMAN_H 0)
   set(HAVE_SYSEXITS_H 0)
   set(HAVE_UNISTD_H 0)
+  set(HAVE_SYS_IOCTL_H 0)
 elseif (ZOS)
   # Confirmed in
   # https://github.com/llvm/llvm-project/pull/104706#issuecomment-2297109613
@@ -42,6 +45,7 @@ elseif (ZOS)
   set(HAVE_SYS_MMAN_H 1)
   set(HAVE_SYSEXITS_H 0)
   set(HAVE_UNISTD_H 1)
+  set(HAVE_SYS_IOCTL_H 1)
 else()
   # Other platforms that we don't promise support for.
   check_include_file(mach/mach.h HAVE_MACH_MACH_H)
@@ -50,6 +54,7 @@ else()
   check_include_file(sys/mman.h HAVE_SYS_MMAN_H)
   check_include_file(sysexits.h HAVE_SYSEXITS_H)
   check_include_file(unistd.h HAVE_UNISTD_H)
+  check_include_file(sys/ioctl.h HAVE_SYS_IOCTL_H)
 endif()
 
 if( UNIX AND NOT (APPLE OR BEOS OR HAIKU) )
diff --git a/llvm/include/llvm/Config/config.h.cmake b/llvm/include/llvm/Config/config.h.cmake
index 06d4756397911..ce83de8e4cba9 100644
--- a/llvm/include/llvm/Config/config.h.cmake
+++ b/llvm/include/llvm/Config/config.h.cmake
@@ -164,6 +164,9 @@
 /* Define to 1 if you have the <sys/mman.h> header file. */
 #cmakedefine HAVE_SYS_MMAN_H ${HAVE_SYS_MMAN_H}
 
+/* Define to 1 if you have the <sys/ioctl.h> header file. */
+#cmakedefine HAVE_SYS_IOCTL_H ${HAVE_SYS_IOCTL_H}
+
 /* Define to 1 if stat struct has st_mtimespec member .*/
 #cmakedefine HAVE_STRUCT_STAT_ST_MTIMESPEC_TV_NSEC ${HAVE_STRUCT_STAT_ST_MTIMESPEC_TV_NSEC}
 
diff --git a/llvm/lib/Support/Unix/Process.inc b/llvm/lib/Support/Unix/Process.inc
index b5c3719f57963..db735b7484ad8 100644
--- a/llvm/lib/Support/Unix/Process.inc
+++ b/llvm/lib/Support/Unix/Process.inc
@@ -34,6 +34,9 @@
 #ifdef HAVE_GETAUXVAL
 #include <sys/auxv.h>
 #endif
+#ifdef HAVE_SYS_IOCTL_H
+#include <sys/ioctl.h>
+#endif
 
 //===----------------------------------------------------------------------===//
 //=== WARNING: Implementation here must contain only generic UNIX code that
@@ -304,31 +307,40 @@ bool Process::FileDescriptorIsDisplayed(int fd) {
 #endif
 }
 
-static unsigned getColumns() {
+static unsigned getColumns(int FileID) {
   // If COLUMNS is defined in the environment, wrap to that many columns.
+  // This matches GCC.
   if (const char *ColumnsStr = std::getenv("COLUMNS")) {
     int Columns = std::atoi(ColumnsStr);
     if (Columns > 0)
       return Columns;
   }
 
-  // We used to call ioctl TIOCGWINSZ to determine the width. It is considered
-  // unuseful.
-  return 0;
+  // Some shells do not export COLUMNS; query the column count via ioctl()
+  // instead if it isn't available.
+  unsigned Columns = 0;
+
+#ifdef HAVE_SYS_IOCTL_H
+  struct winsize ws;
+  if (ioctl(FileID, TIOCGWINSZ, &ws) == 0)
+    Columns = ws.ws_col;
+#endif
+
+  return Columns;
 }
 
 unsigned Process::StandardOutColumns() {
   if (!StandardOutIsDisplayed())
     return 0;
 
-  return getColumns();
+  return getColumns(0);
 }
 
 unsigned Process::StandardErrColumns() {
   if (!StandardErrIsDisplayed())
     return 0;
 
-  return getColumns();
+  return getColumns(1);
 }
 
 static bool terminalHasColors() {