From ceb7eb523049fbf96621a231f8f2f16c97fe41d0 Mon Sep 17 00:00:00 2001 From: AaronSteen Date: Wed, 19 Nov 2025 16:48:14 -0800 Subject: [PATCH] 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 --- CMakeLists.txt | 1 + src/nvim/CMakeLists.txt | 2 + src/tee/CMakeLists.txt | 10 ++++ src/tee/tee.c | 130 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 143 insertions(+) create mode 100644 src/tee/CMakeLists.txt create mode 100644 src/tee/tee.c diff --git a/CMakeLists.txt b/CMakeLists.txt index f0a58a828f..a7efecfc1c 100644 --- 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 index d254b1d4ac..83dbce44d4 100644 --- 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 new file mode 100644 index 0000000000..1395c513ae --- /dev/null +++ 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 new file mode 100644 index 0000000000..d8ef4c8aac --- /dev/null +++ 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 +#endif +#include +#include +#include +#include + +#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); +}