Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .agents/docs/2026-05-20-msvc-stl-discovery-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# MSVC STL Discovery — msvc.cppm 模块设计

## 问题

mcpp 在 Windows 上查找 `import std` 的 MSVC STL `std.ixx` 时硬编码了
`C:\Program Files\Microsoft Visual Studio\2022`,导致非标准 VS 安装找不到。

## 方案

新建 `src/toolchain/msvc.cppm` 模块,提供 MSVC/Visual Studio 发现能力。

### 查找策略(按优先级)

1. **vswhere.exe** — 微软官方 VS 安装发现工具,最可靠
- 路径: `C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe`
- 命令: `vswhere -latest -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath`
- 返回: VS 安装路径如 `C:\Program Files\Microsoft Visual Studio\2022\Community`

2. **环境变量** — `VSINSTALLDIR`、`VS170COMNTOOLS`(2022)、`VS160COMNTOOLS`(2019)
- 从 `VSINSTALLDIR` 直接得到 VS 根目录
- 从 `VS*COMNTOOLS` 向上推算

3. **固定路径扫描** — 兜底方案
- `C:\Program Files\Microsoft Visual Studio\{2025,2022,2019}\{Enterprise,Professional,Community,BuildTools}`
- `C:\Program Files (x86)\Microsoft Visual Studio\{2019,2017}\...`

### std.ixx 路径推算

从 VS 安装路径推算:
```
<VS_ROOT>/VC/Tools/MSVC/<version>/modules/std.ixx
```
`<version>` 通过遍历 `VC/Tools/MSVC/` 目录取最新版本。

### 模块接口

```cpp
export module mcpp.toolchain.msvc;
import std;

export namespace mcpp::toolchain::msvc {

// 查找 VS 安装路径(返回最新版本)
std::optional<std::filesystem::path> find_vs_install_path();

// 查找 MSVC 工具链根目录 (VC/Tools/MSVC/<ver>/)
std::optional<std::filesystem::path> find_msvc_tools_dir();

// 查找 std.ixx 模块源文件
std::optional<std::filesystem::path> find_std_module_source();

// 查找 cl.exe(未来 MSVC 工具链支持用)
std::optional<std::filesystem::path> find_cl();

}
```

### 改动点

1. 新建 `src/toolchain/msvc.cppm`
2. `src/toolchain/clang.cppm` — `enrich_toolchain` 中的硬编码 VS 路径改为调用 `msvc::find_std_module_source()`
24 changes: 5 additions & 19 deletions src/toolchain/clang.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export module mcpp.toolchain.clang;

import std;
import mcpp.toolchain.model;
import mcpp.toolchain.msvc;
import mcpp.toolchain.probe;
import mcpp.xlings;
import mcpp.platform;
Expand Down Expand Up @@ -146,26 +147,11 @@ void enrich_toolchain(Toolchain& tc, const std::string& envPrefix) {

#if defined(_WIN32)
// Fallback: if libc++ std.cppm not found, look for MSVC STL's std.ixx.
// This happens when Clang targets x86_64-pc-windows-msvc.
// Uses msvc.cppm which searches via vswhere, env vars, and known paths.
if (!tc.hasImportStd && msvTarget) {
// Search Visual Studio installations for std.ixx
// Typical path: C:\Program Files\Microsoft Visual Studio\2022\*\VC\Tools\MSVC\*\modules\std.ixx
std::error_code ec;
std::filesystem::path vsBase = "C:\\Program Files\\Microsoft Visual Studio\\2022";
if (std::filesystem::exists(vsBase, ec)) {
for (auto& edition : std::filesystem::directory_iterator(vsBase, ec)) {
auto vcTools = edition.path() / "VC" / "Tools" / "MSVC";
if (!std::filesystem::exists(vcTools, ec)) continue;
for (auto& ver : std::filesystem::directory_iterator(vcTools, ec)) {
auto stdIxx = ver.path() / "modules" / "std.ixx";
if (std::filesystem::exists(stdIxx, ec)) {
tc.stdModuleSource = stdIxx;
tc.hasImportStd = true;
break;
}
}
if (tc.hasImportStd) break;
}
if (auto p = mcpp::toolchain::msvc::find_std_module_source()) {
tc.stdModuleSource = *p;
tc.hasImportStd = true;
}
}
#endif
Expand Down
197 changes: 197 additions & 0 deletions src/toolchain/msvc.cppm
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
// mcpp.toolchain.msvc — MSVC / Visual Studio discovery on Windows.
//
// Provides reliable discovery of Visual Studio installations and MSVC
// toolchain components (std.ixx, cl.exe, lib.exe, etc.) using multiple
// strategies:
// 1. vswhere.exe (Microsoft's official VS locator)
// 2. Environment variables (VSINSTALLDIR, VS*COMNTOOLS)
// 3. Well-known installation paths (fallback)
//
// This module is used by clang.cppm to find MSVC STL's std.ixx when
// Clang targets x86_64-pc-windows-msvc. It will also serve as the
// foundation for future native MSVC (cl.exe) toolchain support.

module;
#include <cstdio>
#include <cstdlib>
#if defined(_WIN32)
#define popen _popen
#define pclose _pclose
#endif

export module mcpp.toolchain.msvc;

import std;

export namespace mcpp::toolchain::msvc {

// Find a Visual Studio installation path (returns the newest found).
std::optional<std::filesystem::path> find_vs_install_path();

// Find the MSVC tools directory: <VS>/VC/Tools/MSVC/<latest_version>/
std::optional<std::filesystem::path> find_msvc_tools_dir();

// Find MSVC STL's std.ixx module source file.
std::optional<std::filesystem::path> find_std_module_source();

// Find cl.exe (for future MSVC toolchain support).
std::optional<std::filesystem::path> find_cl();

} // namespace mcpp::toolchain::msvc

namespace mcpp::toolchain::msvc {

namespace {

#if defined(_WIN32)

// Run a command and capture stdout (first line, trimmed).
std::string run_capture_line(const std::string& cmd) {
std::array<char, 4096> buf{};
std::string out;
std::FILE* fp = ::popen(cmd.c_str(), "r");
if (!fp) return {};
while (std::fgets(buf.data(), buf.size(), fp) != nullptr)
out += buf.data();
::pclose(fp);
// Trim trailing whitespace/newlines
while (!out.empty() && (out.back() == '\n' || out.back() == '\r' || out.back() == ' '))
out.pop_back();
// Take first line only
auto nl = out.find('\n');
if (nl != std::string::npos) out.resize(nl);
return out;
}

// Strategy 1: Use vswhere.exe to find VS installation.
std::optional<std::filesystem::path> find_vs_via_vswhere() {
// vswhere.exe ships with the VS Installer at a well-known path
std::filesystem::path vswhere =
"C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe";
if (!std::filesystem::exists(vswhere)) return std::nullopt;

auto result = run_capture_line(
"\"" + vswhere.string() + "\" -latest -products * "
"-requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 "
"-property installationPath 2>nul");

if (!result.empty() && std::filesystem::exists(result))
return std::filesystem::path(result);
return std::nullopt;
}

// Strategy 2: Use environment variables.
std::optional<std::filesystem::path> find_vs_via_env() {
// VSINSTALLDIR is set inside VS Developer Command Prompt
if (auto* dir = std::getenv("VSINSTALLDIR"); dir && *dir) {
std::filesystem::path p{dir};
if (std::filesystem::exists(p / "VC" / "Tools" / "MSVC"))
return p;
}

// VS*COMNTOOLS: VS170COMNTOOLS (2022), VS160COMNTOOLS (2019), VS150COMNTOOLS (2017)
for (auto* var : {"VS170COMNTOOLS", "VS160COMNTOOLS", "VS150COMNTOOLS"}) {
if (auto* val = std::getenv(var); val && *val) {
// Common7/Tools/ → go up two levels to VS root
std::filesystem::path p{val};
auto root = p.parent_path().parent_path();
if (std::filesystem::exists(root / "VC" / "Tools" / "MSVC"))
return root;
}
}
return std::nullopt;
}

// Strategy 3: Scan well-known paths.
std::optional<std::filesystem::path> find_vs_via_paths() {
static constexpr std::string_view bases[] = {
"C:\\Program Files\\Microsoft Visual Studio",
"C:\\Program Files (x86)\\Microsoft Visual Studio",
};
static constexpr std::string_view years[] = {"2025", "2022", "2019", "2017"};
static constexpr std::string_view editions[] = {
"Enterprise", "Professional", "Community", "BuildTools", "Preview"
};

std::error_code ec;
for (auto base : bases) {
for (auto year : years) {
for (auto edition : editions) {
auto p = std::filesystem::path(base) / std::string(year) / std::string(edition);
if (std::filesystem::exists(p / "VC" / "Tools" / "MSVC", ec))
return p;
}
}
}
return std::nullopt;
}

// From a VS install path, find the latest MSVC tools version directory.
std::optional<std::filesystem::path> find_latest_msvc_tools(const std::filesystem::path& vsRoot) {
auto vcTools = vsRoot / "VC" / "Tools" / "MSVC";
std::error_code ec;
if (!std::filesystem::exists(vcTools, ec)) return std::nullopt;

std::filesystem::path latest;
std::string latestVer;
for (auto& entry : std::filesystem::directory_iterator(vcTools, ec)) {
if (!entry.is_directory()) continue;
auto ver = entry.path().filename().string();
if (ver > latestVer) {
latestVer = ver;
latest = entry.path();
}
}
return latest.empty() ? std::nullopt : std::optional{latest};
}

#endif // _WIN32

} // namespace

std::optional<std::filesystem::path> find_vs_install_path() {
#if defined(_WIN32)
// Try strategies in order of reliability
if (auto p = find_vs_via_vswhere()) return p;
if (auto p = find_vs_via_env()) return p;
if (auto p = find_vs_via_paths()) return p;
#endif
return std::nullopt;
}

std::optional<std::filesystem::path> find_msvc_tools_dir() {
#if defined(_WIN32)
auto vs = find_vs_install_path();
if (!vs) return std::nullopt;
return find_latest_msvc_tools(*vs);
#else
return std::nullopt;
#endif
}

std::optional<std::filesystem::path> find_std_module_source() {
#if defined(_WIN32)
auto tools = find_msvc_tools_dir();
if (!tools) return std::nullopt;

auto stdIxx = *tools / "modules" / "std.ixx";
if (std::filesystem::exists(stdIxx))
return stdIxx;
#endif
return std::nullopt;
}

std::optional<std::filesystem::path> find_cl() {
#if defined(_WIN32)
auto tools = find_msvc_tools_dir();
if (!tools) return std::nullopt;

// cl.exe is at <tools>/bin/Hostx64/x64/cl.exe
auto cl = *tools / "bin" / "Hostx64" / "x64" / "cl.exe";
if (std::filesystem::exists(cl))
return cl;
#endif
return std::nullopt;
}

} // namespace mcpp::toolchain::msvc
Loading