Skip to content

Commit 8ecdbe8

Browse files
committed
[Clang] Match MSVC handling of duplicate header search paths in Microsoft compatibility modes.
Clang has historically matched GCC's behavior for header search path order and pruning of duplicate paths. That traditional behavior is to order user search paths before system search paths, to ignore user search paths that duplicate a (later) system search path, and to ignore search paths that duplicate an earlier search path of the same user/system kind. This differs from MSVC and can result in inconsistent header file resolution for `#include` directives. MSVC orders header search paths as follows: 1) Paths specified by the `/I` and `/external:I` options are processed in the order that they appear. Paths specified by `/I` that duplicate a path specified by `/external:I` are ignored regardless of the order of the options. Paths specified by `/I` that duplicate a path from a prior `/I` option are ignored. Paths specified by `/external:I` that duplicate a path from a later `/external:I` option are ignored. 2) Paths specified by `/external:env` are processed in the order that they appear. Paths that duplicate a path from a `/I` or `/external:I` option are ignored regardless of the order of the options. Paths that duplicate a path from a prior `/external:env` option or an earlier path from the same `/external:env` option are ignored. 3) Paths specified by the `INCLUDE` environment variable are processed in the order they appear. Paths that duplicate a path from a `/I`, `/external:I`, or `/external:env` option are ignored. Paths that duplicate an earlier path in the `INCLUDE` environment variable are ignored. 4) Paths specified by the `EXTERNAL_INCLUDE` environment variable are processed in the order they appear. Paths that duplicate a path from a `/I`, `/external:I`, or `/external:env` option are ignored. Paths that duplicate a path from the `INCLUDE` environment variable are ignored. Paths that duplicate an earlier path in the `EXTERNAL_INCLUDE environment variable are ignored. Prior to this change, Clang handled the `/external:I` and `/external:env` options and the paths present in the `INCLUDE` and `EXTERNAL_INCLUDE` environment variables as though they were specified with the `-isystem` option. The GCC behavior described above then lead to a command line such as `/external:I dir1 /Idir2` having a header search order of `dir2` followed by `dir1`; contrary to MSVC behavior. This change adds support for the MSVC external path concept for both the `clang` and `clang-cl` drivers with the following option syntax. These options match the MSVC behavior described above for both drivers. clang clang-cl -------------------- ------------------- -iexternal <dir> /external:I <dir> -iexternal-env=<ENV> /external:env:<ENV> Paths specified by these options are still treated as system paths. That is, whether warnings are issued in header files found via these paths remains subject to use of the `-Wsystem-headers` and `-Wno-system-headers` options. In the future, it would make sense to add a separate option that matches the MSVC `/external:Wn` option to control such warnings. The MSVC behavior described above implies that (system) paths present in the `INCLUDE` and `EXTERNAL_INCLUDE` environment variables do not suppress matching user paths specified via `/I`. This contrasts with GCC's behavior of suppressing user paths that match a system path regardless of how each is specified. Since the `clang-cl` driver maps paths from the `INCLUDE` and `EXTERNAL_INCLUDE` environment variable to `-internal-isystem`, matching MSVC behavior requires suppressing that aspect of the GCC behavior. With this change, system paths will no longer suppress user paths when the `-fms-compatibility` option is explicitly or implicitly enabled. This will affect header search path ordering for options like `-isystem` when duplicate user paths are present. Should motivation arise for preserving such suppression of user paths when compiling with `-fms-compatibility` enabled, it would make sense to introduce a new option for the `clang-cl` driver to map paths in these environment variabless to that would match MSVC behavior without impacting other system path options.
1 parent cf27e8e commit 8ecdbe8

File tree

16 files changed

+1016
-127
lines changed

16 files changed

+1016
-127
lines changed

clang/docs/ReleaseNotes.rst

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -944,6 +944,44 @@ Windows Support
944944
When `-fms-compatibility-version=18.00` or prior is set on the command line this Microsoft extension is still
945945
allowed as VS2013 and prior allow it.
946946

947+
- Clang now matches MSVC behavior regarding the handling of duplicate header
948+
search paths when running in Microsoft compatibility mode. Historically,
949+
Clang has mimicked gcc behavior in which user search paths are ordered before
950+
system search paths, user search paths that duplicate a (later) system search
951+
path are ignored, and search paths that duplicate an earlier search path of
952+
the same user/system kind are ignored. This ordering is not compatible with
953+
the ordering that MSVC uses when paths are duplicated across ``/I`` options
954+
and the ``INCLUDE`` environment variable.
955+
956+
The order that MSVC uses and that Clang now replicates when the
957+
``-fms-compatibility`` option is enabled follows.
958+
959+
- Paths specified by the ``/I`` and ``/external:I`` options are processed in
960+
the order that they appear. Paths specified by ``/I`` that duplicate a path
961+
specified by ``/external:I`` are ignored regardless of the order of the
962+
options. Paths specified by ``/I`` that duplicate a path from a prior ``/I``
963+
option are ignored. Paths specified by ``/external:I`` that duplicate a
964+
path from a later ``/external:I`` option are ignored.
965+
966+
- Paths specified by ``/external:env`` are processed in the order that they
967+
appear. Paths that duplicate a path from a ``/I`` or ``/external:I`` option
968+
are ignored regardless of the order of the options. Paths that duplicate a
969+
path from a prior ``/external:env`` option or an earlier path from the same
970+
``/external:env`` option are ignored.
971+
972+
- Paths specified by the ``INCLUDE`` environment variable are processed in
973+
the order they appear. Paths that duplicate a path from a ``/I``,
974+
``/external:I``, or ``/external:env`` option are ignored. Paths that
975+
duplicate an earlier path in the ``INCLUDE`` environment variable are
976+
ignored.
977+
978+
- Paths specified by the ``EXTERNAL_INCLUDE`` environment variable are
979+
processed in the order they appear. Paths that duplicate a path from a
980+
``/I``, ``/external:I``, or ``/external:env`` option are ignored. Paths that
981+
duplicate a path from the ``INCLUDE`` environment variable are ignored.
982+
Paths that duplicate an earlier path in the ``EXTERNAL_INCLUDE``
983+
environment variable are ignored.
984+
947985
LoongArch Support
948986
^^^^^^^^^^^^^^^^^
949987

clang/include/clang/Driver/Options.td

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4587,6 +4587,15 @@ def iapinotes_modules : JoinedOrSeparate<["-"], "iapinotes-modules">, Group<clan
45874587
def idirafter : JoinedOrSeparate<["-"], "idirafter">, Group<clang_i_Group>,
45884588
Visibility<[ClangOption, CC1Option]>,
45894589
HelpText<"Add directory to AFTER include search path">;
4590+
def iexternal : Separate<["-"], "iexternal">, Group<clang_i_Group>,
4591+
Visibility<[ClangOption, CC1Option]>,
4592+
HelpText<"Add directory to include search path with warnings suppressed">, MetaVarName<"<dir>">;
4593+
def iexternal_after : Separate<["-"], "iexternal-after">, Group<clang_i_Group>,
4594+
Visibility<[ClangOption, CC1Option]>,
4595+
HelpText<"Add directory to include search path with warnings suppressed">, MetaVarName<"<dir>">;
4596+
def iexternal_env_EQ : Joined<["-"], "iexternal-env=">, Group<clang_i_Group>,
4597+
Visibility<[ClangOption]>,
4598+
HelpText<"Add dirs in env var <var> to include search path with warnings suppressed">, MetaVarName<"<var>">;
45904599
def iframework : JoinedOrSeparate<["-"], "iframework">, Group<clang_i_Group>,
45914600
Visibility<[ClangOption, CC1Option]>,
45924601
HelpText<"Add directory to SYSTEM framework search path">;
@@ -8502,9 +8511,12 @@ def _SLASH_diagnostics_classic : CLFlag<"diagnostics:classic">,
85028511
def _SLASH_D : CLJoinedOrSeparate<"D", [CLOption, DXCOption]>,
85038512
HelpText<"Define macro">, MetaVarName<"<macro[=value]>">, Alias<D>;
85048513
def _SLASH_E : CLFlag<"E">, HelpText<"Preprocess to stdout">, Alias<E>;
8505-
def _SLASH_external_COLON_I : CLJoinedOrSeparate<"external:I">, Alias<isystem>,
8514+
def _SLASH_external_COLON_I : CLJoinedOrSeparate<"external:I">, Alias<iexternal>,
85068515
HelpText<"Add directory to include search path with warnings suppressed">,
85078516
MetaVarName<"<dir>">;
8517+
def _SLASH_external_env : CLJoined<"external:env:">, Alias<iexternal_env_EQ>,
8518+
HelpText<"Add dirs in env var <var> to include search path with warnings suppressed">,
8519+
MetaVarName<"<var>">;
85088520
def _SLASH_fp_contract : CLFlag<"fp:contract">, HelpText<"">, Alias<ffp_contract>, AliasArgs<["on"]>;
85098521
def _SLASH_fp_except : CLFlag<"fp:except">, HelpText<"">, Alias<ffp_exception_behavior_EQ>, AliasArgs<["strict"]>;
85108522
def _SLASH_fp_except_ : CLFlag<"fp:except-">, HelpText<"">, Alias<ffp_exception_behavior_EQ>, AliasArgs<["ignore"]>;
@@ -8740,9 +8752,6 @@ def _SLASH_volatile_Group : OptionGroup<"</volatile group>">,
87408752
def _SLASH_EH : CLJoined<"EH">, HelpText<"Set exception handling model">;
87418753
def _SLASH_EP : CLFlag<"EP">,
87428754
HelpText<"Disable linemarker output and preprocess to stdout">;
8743-
def _SLASH_external_env : CLJoined<"external:env:">,
8744-
HelpText<"Add dirs in env var <var> to include search path with warnings suppressed">,
8745-
MetaVarName<"<var>">;
87468755
def _SLASH_FA : CLJoined<"FA">,
87478756
HelpText<"Output assembly code file during compilation">;
87488757
def _SLASH_Fa : CLJoined<"Fa">,

clang/include/clang/Driver/ToolChain.h

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ class ToolChain {
224224
/// \return The subdirectory path if it exists.
225225
std::optional<std::string> getTargetSubDirPath(StringRef BaseDir) const;
226226

227+
public:
227228
/// \name Utilities for implementing subclasses.
228229
///@{
229230
static void addSystemInclude(const llvm::opt::ArgList &DriverArgs,
@@ -239,12 +240,20 @@ class ToolChain {
239240
static void addSystemIncludes(const llvm::opt::ArgList &DriverArgs,
240241
llvm::opt::ArgStringList &CC1Args,
241242
ArrayRef<StringRef> Paths);
243+
static bool addSystemIncludesFromEnv(const llvm::opt::ArgList &DriverArgs,
244+
llvm::opt::ArgStringList &CC1Args,
245+
StringRef Var);
246+
static void addExternalAfterIncludes(const llvm::opt::ArgList &DriverArgs,
247+
llvm::opt::ArgStringList &CC1Args,
248+
ArrayRef<StringRef> Paths);
249+
static bool addExternalIncludesFromEnv(const llvm::opt::ArgList &DriverArgs,
250+
llvm::opt::ArgStringList &CC1Args,
251+
StringRef Var);
242252

243253
static std::string concat(StringRef Path, const Twine &A, const Twine &B = "",
244254
const Twine &C = "", const Twine &D = "");
245255
///@}
246256

247-
public:
248257
virtual ~ToolChain();
249258

250259
// Accessors

clang/include/clang/Lex/HeaderSearchOptions.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,16 @@ enum IncludeDirGroup {
3535
/// Paths for '\#include <>' added by '-I'.
3636
Angled,
3737

38+
/// Like Angled, but marks system directories while retaining relative order
39+
/// with user directories. This group is intended to match the semantics of
40+
/// the MSVC /external:I option.
41+
External,
42+
43+
/// Like External, but searched after other external directories but before
44+
/// system directories. This group is intended to match the semantics of the
45+
/// MSVC /external:env option.
46+
ExternalAfter,
47+
3848
/// Like Angled, but marks system directories.
3949
System,
4050

clang/lib/Driver/Driver.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1306,7 +1306,9 @@ Compilation *Driver::BuildCompilation(ArrayRef<const char *> ArgList) {
13061306
if (VFS->setCurrentWorkingDirectory(WD->getValue()))
13071307
Diag(diag::err_drv_unable_to_set_working_directory) << WD->getValue();
13081308

1309-
// Check for missing include directories.
1309+
// Check for missing include directories. Diagnostics should not be issued
1310+
// for directories specified with -iexternal, -iexternal-env=, or
1311+
// -iexternal-after since those options may be used to specify partial paths.
13101312
if (!Diags.isIgnored(diag::warn_missing_include_dirs, SourceLocation())) {
13111313
for (auto IncludeDir : Args.getAllArgValues(options::OPT_I_Group)) {
13121314
if (!VFS->exists(IncludeDir))

clang/lib/Driver/ToolChain.cpp

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1301,6 +1301,51 @@ void ToolChain::addExternCSystemIncludeIfExists(const ArgList &DriverArgs,
13011301
}
13021302
}
13031303

1304+
/// Utility function to add a list of ';' delimited directories specified in
1305+
/// an environment variable to the system include path list for CC1. Returns
1306+
/// true if the variable is set and not empty.
1307+
/*static*/ bool ToolChain::addSystemIncludesFromEnv(const ArgList &DriverArgs,
1308+
ArgStringList &CC1Args,
1309+
StringRef Var) {
1310+
if (auto Val = llvm::sys::Process::GetEnv(Var)) {
1311+
SmallVector<StringRef, 8> Dirs;
1312+
StringRef(*Val).split(Dirs, ";", /*MaxSplit=*/-1, /*KeepEmpty=*/false);
1313+
if (!Dirs.empty()) {
1314+
addSystemIncludes(DriverArgs, CC1Args, Dirs);
1315+
return true;
1316+
}
1317+
}
1318+
return false;
1319+
}
1320+
1321+
/// Utility function to add a list of directories to the end of the external
1322+
/// include path list for CC1.
1323+
/*static*/ void ToolChain::addExternalAfterIncludes(const ArgList &DriverArgs,
1324+
ArgStringList &CC1Args,
1325+
ArrayRef<StringRef> Paths) {
1326+
for (const auto &Path : Paths) {
1327+
CC1Args.push_back("-iexternal-after");
1328+
CC1Args.push_back(DriverArgs.MakeArgString(Path));
1329+
}
1330+
}
1331+
1332+
/// Utility function to add a list of ';' delimited directories specified in
1333+
/// an environment variable to the external include path list for CC1. Returns
1334+
/// true if the variable is set and not empty.
1335+
/*static*/ bool ToolChain::addExternalIncludesFromEnv(const ArgList &DriverArgs,
1336+
ArgStringList &CC1Args,
1337+
StringRef Var) {
1338+
if (auto Val = llvm::sys::Process::GetEnv(Var)) {
1339+
SmallVector<StringRef, 8> Dirs;
1340+
StringRef(*Val).split(Dirs, ";", /*MaxSplit=*/-1, /*KeepEmpty=*/false);
1341+
if (!Dirs.empty()) {
1342+
addExternalAfterIncludes(DriverArgs, CC1Args, Dirs);
1343+
return true;
1344+
}
1345+
}
1346+
return false;
1347+
}
1348+
13041349
/*static*/ std::string ToolChain::concat(StringRef Path, const Twine &A,
13051350
const Twine &B, const Twine &C,
13061351
const Twine &D) {

clang/lib/Driver/ToolChains/Clang.cpp

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,16 +1198,23 @@ void Clang::AddPreprocessingOptions(Compilation &C, const JobAction &JA,
11981198
} else if (A->getOption().matches(options::OPT_ibuiltininc)) {
11991199
// This is used only by the driver. No need to pass to cc1.
12001200
continue;
1201+
} else if (A->getOption().matches(options::OPT_iexternal)) {
1202+
// This option has to retain relative order with other -I options.
1203+
continue;
1204+
} else if (A->getOption().matches(options::OPT_iexternal_env_EQ)) {
1205+
A->claim();
1206+
ToolChain::addExternalIncludesFromEnv(Args, CmdArgs, A->getValue());
1207+
continue;
12011208
}
12021209

12031210
// Not translated, render as usual.
12041211
A->claim();
12051212
A->render(Args, CmdArgs);
12061213
}
12071214

1208-
Args.addAllArgs(CmdArgs,
1209-
{options::OPT_D, options::OPT_U, options::OPT_I_Group,
1210-
options::OPT_F, options::OPT_embed_dir_EQ});
1215+
Args.addAllArgs(CmdArgs, {options::OPT_D, options::OPT_U,
1216+
options::OPT_I_Group, options::OPT_F,
1217+
options::OPT_iexternal, options::OPT_embed_dir_EQ});
12111218

12121219
// Add -Wp, and -Xpreprocessor if using the preprocessor.
12131220

@@ -8678,7 +8685,7 @@ void ClangAs::ConstructJob(Compilation &C, const JobAction &JA,
86788685
(void)Args.hasArg(options::OPT_force__cpusubtype__ALL);
86798686

86808687
// Pass along any -I options so we get proper .include search paths.
8681-
Args.AddAllArgs(CmdArgs, options::OPT_I_Group);
8688+
Args.addAllArgs(CmdArgs, {options::OPT_I_Group, options::OPT_iexternal});
86828689

86838690
// Pass along any --embed-dir or similar options so we get proper embed paths.
86848691
Args.AddAllArgs(CmdArgs, options::OPT_embed_dir_EQ);

clang/lib/Driver/ToolChains/MSVC.cpp

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -648,24 +648,6 @@ void MSVCToolChain::AddClangSystemIncludeArgs(const ArgList &DriverArgs,
648648
for (const auto &Path : DriverArgs.getAllArgValues(options::OPT__SLASH_imsvc))
649649
addSystemInclude(DriverArgs, CC1Args, Path);
650650

651-
auto AddSystemIncludesFromEnv = [&](StringRef Var) -> bool {
652-
if (auto Val = llvm::sys::Process::GetEnv(Var)) {
653-
SmallVector<StringRef, 8> Dirs;
654-
StringRef(*Val).split(Dirs, ";", /*MaxSplit=*/-1, /*KeepEmpty=*/false);
655-
if (!Dirs.empty()) {
656-
addSystemIncludes(DriverArgs, CC1Args, Dirs);
657-
return true;
658-
}
659-
}
660-
return false;
661-
};
662-
663-
// Add %INCLUDE%-like dirs via /external:env: flags.
664-
for (const auto &Var :
665-
DriverArgs.getAllArgValues(options::OPT__SLASH_external_env)) {
666-
AddSystemIncludesFromEnv(Var);
667-
}
668-
669651
// Add DIA SDK include if requested.
670652
if (const Arg *A = DriverArgs.getLastArg(options::OPT__SLASH_diasdkdir,
671653
options::OPT__SLASH_winsysroot)) {
@@ -682,12 +664,14 @@ void MSVCToolChain::AddClangSystemIncludeArgs(const ArgList &DriverArgs,
682664
if (DriverArgs.hasArg(options::OPT_nostdlibinc))
683665
return;
684666

685-
// Honor %INCLUDE% and %EXTERNAL_INCLUDE%. It should have essential search
686-
// paths set by vcvarsall.bat. Skip if the user expressly set a vctoolsdir.
667+
// Add paths from the INCLUDE and EXTERNAL_INCLUDE environment variables if
668+
// neither a vctoolsdir or winsysroot directory has been explicitly specified.
669+
// If any paths are present in these environment variables, then skip adding
670+
// additional system directories.
687671
if (!DriverArgs.getLastArg(options::OPT__SLASH_vctoolsdir,
688672
options::OPT__SLASH_winsysroot)) {
689-
bool Found = AddSystemIncludesFromEnv("INCLUDE");
690-
Found |= AddSystemIncludesFromEnv("EXTERNAL_INCLUDE");
673+
bool Found = addSystemIncludesFromEnv(DriverArgs, CC1Args, "INCLUDE");
674+
Found |= addSystemIncludesFromEnv(DriverArgs, CC1Args, "EXTERNAL_INCLUDE");
691675
if (Found)
692676
return;
693677
}

clang/lib/Frontend/CompilerInvocation.cpp

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3200,19 +3200,27 @@ static void GenerateHeaderSearchArgs(const HeaderSearchOptions &Opts,
32003200
auto It = Opts.UserEntries.begin();
32013201
auto End = Opts.UserEntries.end();
32023202

3203-
// Add -I... and -F... options in order.
3204-
for (; It < End && Matches(*It, {frontend::Angled}, std::nullopt, true);
3203+
// Add the -I..., -F..., and -iexternal options in order.
3204+
for (; It < End && Matches(*It, {frontend::Angled, frontend::External},
3205+
std::nullopt, true);
32053206
++It) {
32063207
OptSpecifier Opt = [It, Matches]() {
32073208
if (Matches(*It, frontend::Angled, true, true))
32083209
return OPT_F;
32093210
if (Matches(*It, frontend::Angled, false, true))
32103211
return OPT_I;
3212+
if (Matches(*It, frontend::External, false, true))
3213+
return OPT_iexternal;
32113214
llvm_unreachable("Unexpected HeaderSearchOptions::Entry.");
32123215
}();
32133216

32143217
GenerateArg(Consumer, Opt, It->Path);
3215-
};
3218+
}
3219+
3220+
// Add the paths for the -iexternal-env= and -iexternal-after options in
3221+
// order.
3222+
for (; It < End && Matches(*It, {frontend::ExternalAfter}, false, true); ++It)
3223+
GenerateArg(Consumer, OPT_iexternal_after, It->Path);
32163224

32173225
// Note: some paths that came from "[-iprefix=xx] -iwithprefixbefore=yy" may
32183226
// have already been generated as "-I[xx]yy". If that's the case, their
@@ -3322,7 +3330,6 @@ static bool ParseHeaderSearchArgs(HeaderSearchOptions &Opts, ArgList &Args,
33223330
llvm::CachedHashString(MacroDef.split('=').first));
33233331
}
33243332

3325-
// Add -I... and -F... options in order.
33263333
bool IsSysrootSpecified =
33273334
Args.hasArg(OPT__sysroot_EQ) || Args.hasArg(OPT_isysroot);
33283335

@@ -3341,12 +3348,21 @@ static bool ParseHeaderSearchArgs(HeaderSearchOptions &Opts, ArgList &Args,
33413348
return A->getValue();
33423349
};
33433350

3344-
for (const auto *A : Args.filtered(OPT_I, OPT_F)) {
3351+
// Add the -I..., -F..., and -iexternal options in order.
3352+
for (const auto *A : Args.filtered(OPT_I, OPT_F, OPT_iexternal)) {
3353+
frontend::IncludeDirGroup Group = frontend::Angled;
3354+
if (A->getOption().matches(OPT_iexternal))
3355+
Group = frontend::External;
33453356
bool IsFramework = A->getOption().matches(OPT_F);
3346-
Opts.AddPath(PrefixHeaderPath(A, IsFramework), frontend::Angled,
3347-
IsFramework, /*IgnoreSysroot=*/true);
3357+
Opts.AddPath(PrefixHeaderPath(A, IsFramework), Group, IsFramework,
3358+
/*IgnoreSysroot=*/true);
33483359
}
33493360

3361+
// Add the -iexternal-env= and -iexternal-after options in order.
3362+
for (const auto *A : Args.filtered(OPT_iexternal_after))
3363+
Opts.AddPath(A->getValue(), frontend::ExternalAfter,
3364+
/*IsFramework=*/false, /*IgnoreSysRoot=*/true);
3365+
33503366
// Add -iprefix/-iwithprefix/-iwithprefixbefore options.
33513367
StringRef Prefix = ""; // FIXME: This isn't the correct default prefix.
33523368
for (const auto *A :

0 commit comments

Comments
 (0)