Skip to content

Commit 3f62db1

Browse files
Windows find_tools: add support for finding Clang (#1506)
1 parent 5e210bd commit 3f62db1

File tree

2 files changed

+217
-2
lines changed

2 files changed

+217
-2
lines changed

.github/workflows/main.yml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,19 @@ jobs:
9696
rust: stable
9797
target: aarch64-apple-ios
9898
no_run: --no-run
99-
- build: windows-aarch64
99+
- build: cross-windows-aarch64
100100
os: windows-latest
101101
rust: stable
102102
target: aarch64-pc-windows-msvc
103103
no_run: --no-run
104+
- build: windows-aarch64
105+
os: windows-11-arm
106+
rust: stable
107+
target: aarch64-pc-windows-msvc
108+
- build: cross-win64
109+
os: windows-11-arm
110+
rust: stable
111+
target: x86_64-pc-windows-msvc
104112
- build: win32
105113
os: windows-2022
106114
rust: stable-i686-msvc

src/windows/find_tools.rs

Lines changed: 208 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,11 @@ impl EnvGetter for StdEnvGetter {
119119
/// - `"x86"`, `"i586"` or `"i686"`
120120
/// - `"arm"` or `"thumbv7a"`
121121
///
122-
/// The `tool` argument is the tool to find (e.g. `cl.exe` or `link.exe`).
122+
/// The `tool` argument is the tool to find. Supported tools include:
123+
/// - MSVC tools: `cl.exe`, `link.exe`, `lib.exe`, etc.
124+
/// - `MSBuild`: `msbuild.exe`
125+
/// - Visual Studio IDE: `devenv.exe`
126+
/// - Clang/LLVM tools: `clang.exe`, `clang++.exe`, `clang-*.exe`, `llvm-*.exe`, `lld.exe`, etc.
123127
///
124128
/// This function will return `None` if the tool could not be found, or it will
125129
/// return `Some(cmd)` which represents a command that's ready to execute the
@@ -168,6 +172,15 @@ pub(crate) fn find_tool_inner(
168172
return impl_::find_devenv(target, env_getter);
169173
}
170174

175+
// Clang/LLVM isn't located in the same location as other tools like
176+
// cl.exe and lib.exe.
177+
if ["clang", "lldb", "llvm", "ld", "lld"]
178+
.iter()
179+
.any(|&t| tool.contains(t))
180+
{
181+
return impl_::find_llvm_tool(tool, target, env_getter);
182+
}
183+
171184
// Ok, if we're here, now comes the fun part of the probing. Default shells
172185
// or shells like MSYS aren't really configured to execute `cl.exe` and the
173186
// various compiler tools shipped as part of Visual Studio. Here we try to
@@ -509,6 +522,44 @@ mod impl_ {
509522
find_tool_in_vs16plus_path(r"MSBuild\Current\Bin\MSBuild.exe", target, "16", env_getter)
510523
}
511524

525+
pub(super) fn find_llvm_tool(
526+
tool: &str,
527+
target: TargetArch,
528+
env_getter: &dyn EnvGetter,
529+
) -> Option<Tool> {
530+
find_llvm_tool_vs17(tool, target, env_getter)
531+
}
532+
533+
fn find_llvm_tool_vs17(
534+
tool: &str,
535+
target: TargetArch,
536+
env_getter: &dyn EnvGetter,
537+
) -> Option<Tool> {
538+
vs16plus_instances(target, "17", env_getter)
539+
.filter_map(|mut base_path| {
540+
base_path.push(r"VC\Tools\LLVM");
541+
let host_folder = match host_arch() {
542+
// The default LLVM bin folder is x86, and there's separate subfolders
543+
// for the x64 and ARM64 host tools.
544+
X86 => "",
545+
X86_64 => "x64",
546+
AARCH64 => "ARM64",
547+
_ => return None,
548+
};
549+
if host_folder != "" {
550+
// E.g. C:\...\VC\Tools\LLVM\x64
551+
base_path.push(host_folder);
552+
}
553+
// E.g. C:\...\VC\Tools\LLVM\x64\bin\clang.exe
554+
base_path.push("bin");
555+
base_path.push(tool);
556+
base_path
557+
.is_file()
558+
.then(|| Tool::with_family(base_path, MSVC_FAMILY))
559+
})
560+
.next()
561+
}
562+
512563
// In MSVC 15 (2017) MS once again changed the scheme for locating
513564
// the tooling. Now we must go through some COM interfaces, which
514565
// is super fun for Rust.
@@ -1058,6 +1109,152 @@ mod impl_ {
10581109
}
10591110
}
10601111

1112+
#[cfg(test)]
1113+
mod tests {
1114+
use super::*;
1115+
use std::path::Path;
1116+
// Import the find function from the module level
1117+
use crate::windows::find_tools::find;
1118+
// Import StdEnvGetter from the parent module
1119+
use crate::windows::find_tools::StdEnvGetter;
1120+
1121+
fn host_arch_to_string(host_arch_value: u16) -> &'static str {
1122+
match host_arch_value {
1123+
X86 => "x86",
1124+
X86_64 => "x64",
1125+
AARCH64 => "arm64",
1126+
_ => panic!("Unsupported host architecture: {}", host_arch_value),
1127+
}
1128+
}
1129+
1130+
#[test]
1131+
fn test_find_cl_exe() {
1132+
// Test that we can find cl.exe for common target architectures
1133+
// and validate the correct host-target combination paths
1134+
// This should pass on Windows CI with Visual Studio installed
1135+
1136+
let target_architectures = ["x64", "x86", "arm64"];
1137+
let mut found_any = false;
1138+
1139+
// Determine the host architecture
1140+
let host_arch_value = host_arch();
1141+
let host_name = host_arch_to_string(host_arch_value);
1142+
1143+
for &target_arch in &target_architectures {
1144+
if let Some(cmd) = find(target_arch, "cl.exe") {
1145+
// Verify the command looks valid
1146+
assert!(
1147+
!cmd.get_program().is_empty(),
1148+
"cl.exe program path should not be empty"
1149+
);
1150+
assert!(
1151+
Path::new(cmd.get_program()).exists(),
1152+
"cl.exe should exist at: {:?}",
1153+
cmd.get_program()
1154+
);
1155+
1156+
// Verify the path contains the correct host-target combination
1157+
// Use case-insensitive comparison since VS IDE uses "Hostx64" while Build Tools use "HostX64"
1158+
let path_str = cmd.get_program().to_string_lossy();
1159+
let path_str_lower = path_str.to_lowercase();
1160+
let expected_host_target_path =
1161+
format!("\\bin\\host{host_name}\\{target_arch}");
1162+
let expected_host_target_path_unix =
1163+
expected_host_target_path.replace("\\", "/");
1164+
1165+
assert!(
1166+
path_str_lower.contains(&expected_host_target_path) || path_str_lower.contains(&expected_host_target_path_unix),
1167+
"cl.exe path should contain host-target combination (case-insensitive) '{}' for {} host targeting {}, but found: {}",
1168+
expected_host_target_path,
1169+
host_name,
1170+
target_arch,
1171+
path_str
1172+
);
1173+
1174+
found_any = true;
1175+
}
1176+
}
1177+
1178+
assert!(found_any, "Expected to find cl.exe for at least one target architecture (x64, x86, or arm64) on Windows CI with Visual Studio installed");
1179+
}
1180+
1181+
#[test]
1182+
fn test_find_llvm_tools() {
1183+
// Test the actual find_llvm_tool function with various LLVM tools
1184+
// This test assumes CI environment has Visual Studio + Clang installed
1185+
// We test against x64 target since clang can cross-compile to any target
1186+
let target_arch = TargetArch::new("x64").expect("Should support x64 architecture");
1187+
let llvm_tools = ["clang.exe", "clang++.exe", "lld.exe", "llvm-ar.exe"];
1188+
1189+
// Determine expected host-specific path based on host architecture
1190+
let host_arch_value = host_arch();
1191+
let expected_host_path = match host_arch_value {
1192+
X86 => "LLVM\\bin", // x86 host
1193+
X86_64 => "LLVM\\x64\\bin", // x64 host
1194+
AARCH64 => "LLVM\\ARM64\\bin", // arm64 host
1195+
_ => panic!("Unsupported host architecture: {}", host_arch_value),
1196+
};
1197+
1198+
let host_name = host_arch_to_string(host_arch_value);
1199+
1200+
let mut found_tools_count = 0;
1201+
1202+
for &tool in &llvm_tools {
1203+
// Test finding LLVM tools using the standard environment getter
1204+
let env_getter = StdEnvGetter;
1205+
let result = find_llvm_tool(tool, target_arch, &env_getter);
1206+
1207+
match result {
1208+
Some(found_tool) => {
1209+
found_tools_count += 1;
1210+
1211+
// Verify the found tool has a valid, non-empty path
1212+
assert!(
1213+
!found_tool.path().as_os_str().is_empty(),
1214+
"Found LLVM tool '{}' should have a non-empty path",
1215+
tool
1216+
);
1217+
1218+
// Verify the tool path actually exists on filesystem
1219+
assert!(
1220+
found_tool.path().exists(),
1221+
"LLVM tool '{}' path should exist: {:?}",
1222+
tool,
1223+
found_tool.path()
1224+
);
1225+
1226+
// Verify the tool path contains the expected tool name
1227+
let path_str = found_tool.path().to_string_lossy();
1228+
assert!(
1229+
path_str.contains(tool.trim_end_matches(".exe")),
1230+
"Tool path '{}' should contain tool name '{}'",
1231+
path_str,
1232+
tool
1233+
);
1234+
1235+
// Verify it's in the correct host-specific VS LLVM directory
1236+
assert!(
1237+
path_str.contains(expected_host_path) || path_str.contains(&expected_host_path.replace("\\", "/")),
1238+
"LLVM tool should be in host-specific VS LLVM directory '{}' for {} host, but found: {}",
1239+
expected_host_path,
1240+
host_name,
1241+
path_str
1242+
);
1243+
}
1244+
None => {}
1245+
}
1246+
}
1247+
1248+
// On CI with VS + Clang installed, we should find at least some LLVM tools
1249+
assert!(
1250+
found_tools_count > 0,
1251+
"Expected to find at least one LLVM tool on CI with Visual Studio + Clang installed for {} host. Found: {}",
1252+
host_name,
1253+
found_tools_count
1254+
);
1255+
}
1256+
}
1257+
10611258
// Given a registry key, look at all the sub keys and find the one which has
10621259
// the maximal numeric value.
10631260
//
@@ -1179,6 +1376,16 @@ mod impl_ {
11791376
None
11801377
}
11811378

1379+
// Finding Clang/LLVM-related tools on unix systems is not currently supported.
1380+
#[inline(always)]
1381+
pub(super) fn find_llvm_tool(
1382+
_tool: &str,
1383+
_target: TargetArch,
1384+
_: &dyn EnvGetter,
1385+
) -> Option<Tool> {
1386+
None
1387+
}
1388+
11821389
/// Attempt to find the tool using environment variables set by vcvars.
11831390
pub(super) fn find_msvc_environment(
11841391
tool: &str,

0 commit comments

Comments
 (0)