https://gcc.gnu.org/bugzilla/show_bug.cgi?id=127770
Bug ID: 127770
Summary: [MinGW] Quoted include lookup through symlinks depends
on absolute vs. relative -I path
Product: gcc
Version: 15.2.0
Status: UNCONFIRMED
Severity: normal
Priority: P3
Component: preprocessor
Assignee: unassigned at gcc dot gnu.org
Reporter: uff1965 at rambler dot ru
Target Milestone: ---
Created attachment 65780
--> https://gcc.gnu.org/bugzilla/attachment.cgi?id=65780&action=edit
The batch file in the archive creates a shortcut!
On Windows (MinGW-w64), #include "..." and __has_include("...") behave
differently for a header reached through a file symbolic link depending on
whether the -I directory is specified as an absolute or relative path, even
though both paths refer to the same directory.
Compiler: g++.exe (MinGW-W64 x86_64-msvcrt-posix-seh, built by Brecht Sanders,
r6) 15.2.0
--- Reproducer ---
Layout:
include\a.h -> ..\real\a.h (file symbolic link, created with
`mklink include\a.h ..\real\a.h`)
real\a.h
real\b.h
src\main.cpp
real\a.h:
#pragma once
#if __has_include("b.h")
# include "b.h"
#else
# pragma message("NOT FOUND b.h")
#endif
real\b.h:
#pragma once
#pragma message("IN b.h")
src\main.cpp:
#include "a.h"
int main() {}
Commands (run from the project root, C:\Temp\gcc-windows-symlink-include-path,
so that "include" below refers to
C:\Temp\gcc-windows-symlink-include-path\include):
cd C:\Temp\gcc-windows-symlink-include-path
rem Absolute -I
g++ -I"C:\Temp\gcc-windows-symlink-include-path\include" -H -c src/main.cpp
-o nul
rem Relative -I (same directory)
g++ -I"include" -H -c src/main.cpp -o nul
--- Actual results ---
Absolute -I:
. C:/Temp/gcc-windows-symlink-include-path/real/a.h
.. C:/Temp/gcc-windows-symlink-include-path/real/b.h
note: '#pragma message: IN b.h'
GCC reports the symlink target path, and "b.h" is found next to the target.
Relative -I:
. include/a.h
note: '#pragma message: NOT FOUND b.h'
GCC reports the symlink path, and "b.h" is looked up next to the link and is
not found.
--- Expected results ---
Both invocations should behave identically. Equivalent spellings of the same
-I directory should not change the outcome of quoted-header lookup.
Either behavior would be self-consistent; the current one is not.
--- Analysis ---
In libcpp/files.cc (checked on the releases/gcc-15 branch; the same code is
present on master), find_file_in_dir() contains:
/* We try to canonicalize system headers. For DOS based file
* system, we always try to shorten non-system headers, as DOS
* has a tighter constraint on max path length. */
if ((CPP_OPTION (pfile, canonical_system_headers) && file->dir->sysp)
#ifdef HAVE_DOS_BASED_FILE_SYSTEM
|| !file->dir->sysp
#endif
)
{
char *canonical_path = maybe_shorter_path (path);
...
maybe_shorter_path() calls lrealpath() and returns the result only if it is
strictly shorter than the original string. The returned path replaces
file->path. For quoted includes, search_path_head() then derives the
"current file directory" from file->path via dir_name_of_file() (which strips
the basename with lbasename()), so the lookup of "b.h" starts in the
directory of whichever path was kept.
In the reproducer:
* Absolute -I: the resolved path ".../real/a.h" is shorter than
".../include/a.h" ("real" is 3 characters shorter than "include"), so it
replaces file->path and the symlink is effectively resolved.
* Relative -I: "include/a.h" is shorter than the absolute path returned by
lrealpath(), so the original path is kept and the symlink is not resolved.
So the observable semantics of #include "..." and __has_include("...") depend
on the relative lengths of two path strings, not on anything the user wrote
about the header or the search path.
Confirmation: the same layout, but with the directory containing the link
renamed from "include" to "inc" (inc\a.h -> ..\real\a.h), so that the link
path is now shorter than the link target path. The absolute-path case then
behaves like the relative one:
g++ -I"C:\Temp\gcc-windows-symlink-include-path\inc" -H -c src/main.cpp -o
nul
. C:\Temp\gcc-windows-symlink-include-path\inc/a.h
note: '#pragma message: NOT FOUND b.h'
The path printed by -H keeps the original mixed separators (as assembled from
-I and the file name), i.e. it was not replaced by the output of lrealpath().
In the first run, where the path was replaced, it was printed with forward
slashes only (".../real/a.h"). Renaming the link directory from "include" to
"inc" changes the relative lengths of the link path and the target path; no
source-file contents or compiler options are changed.
-fno-canonical-system-headers does not help for plain -I directories, because
on DOS-based file systems the "|| !file->dir->sysp" branch applies regardless
of that option.
--- Related reports ---
PR96027 ("Windows: gcc does not resolve long relative header paths
correctly") also describes include lookup on Windows depending on the lengths
of relative and absolute paths, and may involve the same code path.
PR61922 ("Recursive #include overruns Win32 MAX_PATH due to lack of path
canonization") reports that chained relative #includes accumulate path
components until CreateFile fails at MAX_PATH. Comment 2 there notes that
canonicalization is complicated by symbolic links, and comment 6 argues that
purely textual canonicalization is viable on Windows. The canonicalization in
find_file_in_dir() may be related to that report, so any fix here should
preserve the goal of limiting path growth.
--- Suggested direction ---
The canonicalization is understandable as a path-length mitigation, but it
should not change the directory used for resolving subsequent quoted includes
and __has_include in a way that depends on whether the canonical form happens
to be shorter.
One possible approach might be to normalize paths lexically (removing "." and
".." components) rather than resolving symbolic links with lrealpath(), which
would still limit path growth in recursive includes while keeping lookup
relative to the directory in which the header was found. Alternatively,
symbolic links could be resolved consistently or not at all, independently of
the relative lengths of the two strings.
--- Notes ---
Creating file symlinks on Windows requires administrator rights or Developer
Mode. The attached archive contains the reproducer and a test.cmd that creates
the link and runs both commands. The "inc" variant from the Confirmation
section is obtained by renaming the "include" directory to "inc" and
recreating the link (`mklink inc\a.h ..\real\a.h`).