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>
This commit is contained in:
AaronSteen
2025-11-19 16:48:14 -08:00
committed by GitHub
parent 4f97239661
commit ceb7eb5230
4 changed files with 143 additions and 0 deletions

View File

@@ -312,6 +312,7 @@ add_custom_target(nvim ALL)
add_dependencies(nvim nvim_bin nvim_runtime_deps nvim_runtime) add_dependencies(nvim nvim_bin nvim_runtime_deps nvim_runtime)
add_subdirectory(src/nvim) add_subdirectory(src/nvim)
add_subdirectory(src/tee)
add_subdirectory(cmake.config) add_subdirectory(cmake.config)
add_subdirectory(runtime) add_subdirectory(runtime)
add_subdirectory(test) add_subdirectory(test)

View File

@@ -423,6 +423,7 @@ list(SORT NVIM_SOURCES)
list(SORT NVIM_HEADERS) list(SORT NVIM_HEADERS)
list(APPEND UNCRUSTIFY_NVIM_SOURCES ${NVIM_SOURCES} ${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}) foreach(sfile ${NVIM_SOURCES})
get_filename_component(f ${sfile} NAME) get_filename_component(f ${sfile} NAME)
@@ -454,6 +455,7 @@ foreach(hfile ${NVIM_HEADERS})
endforeach() endforeach()
list(APPEND LINT_NVIM_SOURCES ${NVIM_SOURCES} ${NVIM_HEADERS}) 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) # Log level (NVIM_LOG_DEBUG in log.h)
if(CI_BUILD) if(CI_BUILD)

10
src/tee/CMakeLists.txt Normal file
View File

@@ -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}"
)

130
src/tee/tee.c Normal file
View File

@@ -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);
}