commit ceb7eb523049fbf96621a231f8f2f16c97fe41d0
parent 4f97239661ebf569fa701f211dd92ef1919bc922
Author: AaronSteen <ams5661@gmail.com>
Date: Wed, 19 Nov 2025 16:48:14 -0800
build(windows): restore tee.exe on Windows #36363
Problem:
Neovim no longer ships with a tee binary on Windows, which breaks
functionality for the :grep and :make commands.
nvim --clean
:grep foo or :make
"tee is not recognized as an internal or external command"
Solution:
Include a simple, no-dependency tee.c source file in the src/ directory.
Update CMakeLists.txt to build a tee executable alongside neovim during
the build process, and ensure the tee.exe program appears alongside the
neovim executable in the bin/ directory so that it is accessible for
:grep and :make.
tee.c was obtained from the vim codebase:
https://github.com/vim/vim/blob/master/src/tee/tee.c
And we modified it to fix performance issues.
Testing:
nvim --clean
:grep foo or :make, after setting a file to the makeprg option.
Verify that :grep results and error output from a compiler appear in the message pane.
ref https://github.com/neovim/neovim/issues/32431
fix https://github.com/neovim/neovim/issues/32504
Other tee options:
- [tee-win32](https://github.com/dEajL3kA/tee-win32): MIT. However,
I couldn't get it to build on my machine even after updating its
makefile to call my install of MSVC. It's also super optimized and
uses some processor intrinsics for multithreading.
- [gnu coreutils tee](https://gnuwin32.sourceforge.net/packages/coreutils.htm):
(Windows coreutils contains a tee.c. Last updated 2005. Did not build
immediately on my machine; we'd have to determine which definitions
from elsewhere in coreutils tee.c needs and incorporate them somehow.
- [WinTee](https://github.com/mpderbec/WinTee): Has no license. Last
updated 11 years ago. Relies on Visual Studio to build.
Co-authored-by: Justin M. Keyes <justinkz@gmail.com>
Diffstat:
4 files changed, 143 insertions(+), 0 deletions(-)
diff --git a/CMakeLists.txt b/CMakeLists.txt
@@ -312,6 +312,7 @@ add_custom_target(nvim ALL)
add_dependencies(nvim nvim_bin nvim_runtime_deps nvim_runtime)
add_subdirectory(src/nvim)
+add_subdirectory(src/tee)
add_subdirectory(cmake.config)
add_subdirectory(runtime)
add_subdirectory(test)
diff --git a/src/nvim/CMakeLists.txt b/src/nvim/CMakeLists.txt
@@ -423,6 +423,7 @@ list(SORT NVIM_SOURCES)
list(SORT NVIM_HEADERS)
list(APPEND UNCRUSTIFY_NVIM_SOURCES ${NVIM_SOURCES} ${NVIM_HEADERS})
+list(APPEND UNCRUSTIFY_NVIM_SOURCES ${PROJECT_SOURCE_DIR}/src/tee/tee.c)
foreach(sfile ${NVIM_SOURCES})
get_filename_component(f ${sfile} NAME)
@@ -454,6 +455,7 @@ foreach(hfile ${NVIM_HEADERS})
endforeach()
list(APPEND LINT_NVIM_SOURCES ${NVIM_SOURCES} ${NVIM_HEADERS})
+list(APPEND LINT_NVIM_SOURCES ${PROJECT_SOURCE_DIR}/src/tee/tee.c)
# Log level (NVIM_LOG_DEBUG in log.h)
if(CI_BUILD)
diff --git a/src/tee/CMakeLists.txt b/src/tee/CMakeLists.txt
@@ -0,0 +1,10 @@
+add_executable(tee tee.c)
+
+# tee has to be in the same directory as the nvim executable
+set_target_properties(tee PROPERTIES
+ RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin"
+)
+
+install(TARGETS tee
+ RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}"
+)
diff --git a/src/tee/tee.c b/src/tee/tee.c
@@ -0,0 +1,130 @@
+// tee.c - pipe fitting
+//
+// Copyright (c) 1996, Paul Slootman
+//
+// Author: Paul Slootman
+// (paul@wurtel.hobby.nl, paul@murphy.nl, paulS@toecompst.nl)
+// Modifications for MSVC: Yasuhiro Matsumoto
+// Modifications for Neovim: https://github.com/neovim/neovim/pull/36363
+//
+// This source code is released into the public domain. It is provided on an
+// as-is basis and no responsibility is accepted for its failure to perform
+// as expected. It is worth at least as much as you paid for it!
+//
+//
+// tee reads stdin, and writes what it reads to each of the specified
+// files. The primary reason of existence for this version is a quick
+// and dirty implementation to distribute with Vim, to make one of the
+// most useful features of Vim possible on OS/2: quickfix.
+//
+// Of course, not using tee but instead redirecting make's output directly
+// into a temp file and then processing that is possible, but if we have a
+// system capable of correctly piping (unlike DOS, for example), why not
+// use it as well as possible? This tee should also work on other systems,
+// but it's not been tested there, only on OS/2.
+//
+// tee is also available in the GNU shellutils package, which is available
+// precompiled for OS/2. That one probably works better.
+
+#ifndef _MSC_VER
+# include <unistd.h>
+#endif
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#ifdef _WIN32
+# define sysconf(x) - 1
+#endif
+
+void usage(void)
+{
+ fprintf(stderr,
+ "Neotee: a web-scale fork of tee\n"
+ "Usage:\n"
+ "\ttee [-a] file ... file_n\n"
+ "\n"
+ "\t-a\tappend to files instead of truncating\n"
+ "\n"
+ "Tee reads its input, and writes to each of the specified files,\n"
+ "as well as to the standard output.\n"
+ "\n"
+ "Shipped with Nvim 0.12+ for use with :make, :grep, :!, etc.\n");
+}
+
+int main(int argc, char *argv[])
+{
+ int append = 0;
+ size_t numfiles;
+ int maxfiles;
+ FILE **filepointers;
+ int i;
+ char buf[65536];
+ int n;
+ int optnr = 1;
+
+ for (i = 1; i < argc; i++) {
+ if (argv[i][0] != '-') {
+ break;
+ }
+ if (!strcmp(argv[i], "-a")) {
+ append++;
+ } else {
+ usage();
+ exit(2);
+ }
+ optnr++;
+ }
+
+ numfiles = argc - optnr;
+
+ if (numfiles == 0) {
+ usage();
+ exit(2);
+ }
+
+ maxfiles = sysconf(_SC_OPEN_MAX); // or fill in 10 or so
+ if (maxfiles < 0) {
+ maxfiles = 10;
+ }
+ if (numfiles + 3 > maxfiles) { // +3 accounts for stdin, out, err
+ fprintf(stderr, "There is a limit of max %d files.\n", maxfiles - 3);
+ exit(1);
+ }
+ filepointers = calloc(numfiles, sizeof(FILE *)); // NOLINT
+ if (filepointers == NULL) {
+ fprintf(stderr, "Error allocating memory for %ld files\n", (long)numfiles);
+ exit(1);
+ }
+ for (i = 0; i < numfiles; i++) {
+ filepointers[i] = fopen(argv[i + optnr], append ? "ab" : "wb");
+ if (filepointers[i] == NULL) {
+ fprintf(stderr, "Can't open \"%s\"\n", argv[i + optnr]);
+ exit(1);
+ }
+ }
+#ifdef _WIN32
+ setmode(fileno(stdin), O_BINARY);
+ fflush(stdout); // needed for _fsetmode(stdout)
+ setmode(fileno(stdout), O_BINARY);
+ setvbuf(stdout, NULL, _IONBF, 0); // unbuffered for immediate output
+#endif
+
+ while ((n = fread(buf, 1, sizeof(buf), stdin)) > 0) {
+ fwrite(buf, 1, n, stdout);
+ for (i = 0; i < numfiles; i++) {
+ if (filepointers[i]) {
+ fwrite(buf, 1, n, filepointers[i]);
+ }
+ }
+ fflush(stdout);
+ }
+ for (i = 0; i < numfiles; i++) {
+ if (filepointers[i]) {
+ fclose(filepointers[i]);
+ }
+ }
+
+ exit(0);
+}