Install the pinned MSVC toolset from the Manager

Modules must be compiled by the core's exact compiler. The Toolchain tab now
offers Microsoft's stable-channel Build Tools bootstrapper (VS 18) with the
14.44 toolset, core build tools, Windows SDK 26100 and CMake preselected:
downloaded from aka.ms, Authenticode-checked for Microsoft, run in its own UI
(modify mode for an existing Build Tools 18). Shows the required and selected
cl.exe versions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ДмитрийandClaude Opus 5.5 committed 2026-11-05 15:38:58 +05:00
1 parent da94fb49a4
commit 3be5fe9609
6 files changed
+213

No files matched your search

@@ -0,0 +1,7 @@
---
area: manager
type: added
audience: admins
title: Install the compiler for modules from the Manager
---
When the C++ compiler that modules need is missing or differs from the one the core was built with, the Toolchain tab offers to install Visual Studio Build Tools with MSVC 14.44. The Manager downloads Microsoft's installer, checks that Microsoft signed it and opens it with the needed components selected; you accept Microsoft's terms there.
+1
View File
@@ -43,6 +43,7 @@ pub mod logging;
pub mod manifest;
pub mod migrations;
pub mod modules;
pub mod msvc_setup;
pub mod multiworld;
mod mysql_snapshot;
pub mod net;
+146
View File
@@ -0,0 +1,146 @@
//! Installs the C++ compiler that plugins must be built with.
//!
//! A plugin loads only into a core built by the same MSVC, to the build number (it is part of the ABI
//! fingerprint). The core is built with the MSVC 14.44 toolset of the stable Visual Studio channel, so
//! the Manager installs exactly that: Microsoft's own Build Tools bootstrapper, downloaded from Microsoft
//! and checked to be signed by Microsoft, with the components preselected. The person accepts Microsoft's
//! terms in Microsoft's installer window; nothing is bundled or installed silently.
use std::path::{Path, PathBuf};
use std::process::Command;
use serde::Serialize;
use crate::build_engine::{detect_toolchain_for, ToolchainStatus};
use crate::error::{Error, Result};
/// The stable-channel Build Tools bootstrapper (Visual Studio 18). Its MSVC 14.44 side-by-side toolset has
/// the same compiler build as the CI image the core is built on.
pub const BOOTSTRAPPER_URL: &str = "https://aka.ms/vs/18/stable/vs_buildtools.exe";
/// The toolset alone has no environment script; the core build tools and the Windows SDK complete it.
pub const COMPONENTS: [&str; 4] = [
"Microsoft.VisualStudio.Component.VC.14.44.17.14.x86.x64",
"Microsoft.VisualStudio.Component.VC.CoreBuildTools",
"Microsoft.VisualStudio.Component.Windows11SDK.26100",
"Microsoft.VisualStudio.Component.VC.CMake.Project",
];
#[derive(Debug, Clone, Serialize)]
pub struct MsvcSetupOutcome {
/// The installer finished and the toolchain now matches the core.
pub ready: bool,
pub restart_required: bool,
pub message: String,
pub toolchain: ToolchainStatus,
}
/// An installed Visual Studio 18 Build Tools instance, which the installer modifies instead of adding a
/// second one.
fn existing_build_tools() -> Option<PathBuf> {
let pf86 = std::env::var("ProgramFiles(x86)").unwrap_or_else(|_| "C:\\Program Files (x86)".into());
let vswhere = Path::new(&pf86).join("Microsoft Visual Studio").join("Installer").join("vswhere.exe");
let mut cmd = Command::new(vswhere);
cmd.args(["-products", "Microsoft.VisualStudio.Product.BuildTools", "-version", "[18.0,19.0)",
"-property", "installationPath", "-utf8"]);
hide_window(&mut cmd);
let out = cmd.output().ok()?;
String::from_utf8_lossy(&out.stdout).lines().map(str::trim).find(|l| !l.is_empty()).map(PathBuf::from)
.filter(|p| p.is_dir())
}
fn hide_window(cmd: &mut Command) {
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(0x0800_0000);
}
#[cfg(not(windows))]
let _ = cmd;
}
const SCRIPT: &str = r#"param([string]$Dir, [string]$Url, [string]$InstallPath, [string]$Components)
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
New-Item -ItemType Directory -Force -Path $Dir | Out-Null
$bt = Join-Path $Dir 'vs_buildtools.exe'
try { Invoke-WebRequest -UseBasicParsing -Uri $Url -OutFile $bt } catch { "RESULT=download-failed $($_.Exception.Message)"; exit 0 }
$sig = Get-AuthenticodeSignature -FilePath $bt
if ($sig.Status -ne 'Valid' -or $sig.SignerCertificate.Subject -notmatch 'O=Microsoft Corporation') {
Remove-Item $bt -Force
"RESULT=bad-signature $($sig.Status)"; exit 0
}
$a = @()
if ($InstallPath) { $a += @('modify', '--installPath', ('"' + $InstallPath + '"')) }
$a += @('--wait', '--norestart', '--nocache')
foreach ($c in $Components.Split(',')) { $a += @('--add', $c) }
try { $p = Start-Process -FilePath $bt -ArgumentList $a -Wait -PassThru } catch { "RESULT=not-started $($_.Exception.Message)"; exit 0 }
"RESULT=exit $($p.ExitCode)"
"#;
/// Runs Microsoft's installer and reports whether plugins can now be built for `realm_root`'s core.
/// Blocks until the installer window is closed.
pub fn install_build_tools(realm_root: Option<&Path>) -> Result<MsvcSetupOutcome> {
if !cfg!(windows) {
return Err(Error::Invalid("The C++ Build Tools installer is only available on Windows.".into()));
}
let dir = std::env::temp_dir().join("coa-msvc-setup");
std::fs::create_dir_all(&dir)?;
let script = dir.join("install-build-tools.ps1");
std::fs::write(&script, SCRIPT)?;
let install_path = existing_build_tools().map(|p| p.to_string_lossy().into_owned()).unwrap_or_default();
let mut cmd = Command::new("powershell.exe");
cmd.args(["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File"])
.arg(&script)
.args(["-Dir"]).arg(dir.join("bootstrapper"))
.args(["-Url", BOOTSTRAPPER_URL, "-InstallPath", &install_path, "-Components", &COMPONENTS.join(",")]);
hide_window(&mut cmd);
let out = cmd.output()?;
let text = String::from_utf8_lossy(&out.stdout);
let result = text.lines().find_map(|l| l.trim().strip_prefix("RESULT=")).unwrap_or("").to_string();
let (installed, restart, message) = interpret(&result, &String::from_utf8_lossy(&out.stderr));
let toolchain = detect_toolchain_for(realm_root);
let ready = installed && toolchain.ready;
let message = if installed && !toolchain.ready {
format!("The installer finished, but the compiler still does not match this core: {}", toolchain.diagnostic_message)
} else {
message
};
Ok(MsvcSetupOutcome { ready, restart_required: restart, message, toolchain })
}
/// (installed, restart required, message) for the script's result line.
fn interpret(result: &str, stderr: &str) -> (bool, bool, String) {
let (kind, detail) = result.split_once(' ').unwrap_or((result, ""));
match (kind, detail.trim()) {
("exit", "0") => (true, false, "Visual Studio Build Tools with MSVC 14.44 are installed.".into()),
("exit", "3010") => (true, true, "Visual Studio Build Tools are installed; Windows needs a restart before modules can be built.".into()),
("exit", "1602") | ("exit", "1223") => (false, false, "The installation was cancelled.".into()),
("exit", "8006") => (false, false, "Close Visual Studio and its installer, then try again.".into()),
("exit", code) => (false, false, format!("The Visual Studio installer stopped with code {code}.")),
("bad-signature", s) => (false, false, format!("The downloaded installer is not signed by Microsoft ({s}); it was deleted and not run.")),
("download-failed", why) => (false, false, format!("The Visual Studio installer could not be downloaded: {why}")),
("not-started", why) => (false, false, format!("The Visual Studio installer could not be started: {why}")),
_ => (false, false, format!("The installer script failed: {}", stderr.lines().next().unwrap_or("no output"))),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn installer_results_are_explained() {
assert!(interpret("exit 0", "").0);
assert_eq!(interpret("exit 3010", "").1, true);
assert!(!interpret("exit 1602", "").0);
assert!(interpret("bad-signature NotSigned", "").2.contains("not signed by Microsoft"));
assert!(interpret("", "boom").2.contains("boom"));
}
#[test]
fn the_components_name_the_pinned_toolset() {
assert!(COMPONENTS.iter().any(|c| c.contains("VC.14.44")));
assert!(BOOTSTRAPPER_URL.starts_with("https://aka.ms/vs/18/stable/"));
}
}
+11
View File
@@ -2964,6 +2964,16 @@ fn preview_toolchain_status(
coa_core::build_engine::detect_toolchain_for(realm_root.as_deref())
}
/// Runs Microsoft's Build Tools installer with the MSVC toolset this realm's core was built with.
#[tauri::command]
async fn preview_install_msvc(
state: State<'_, AppState>,
realm_id: Option<String>,
) -> std::result::Result<coa_core::msvc_setup::MsvcSetupOutcome, UiError> {
let realm_root = realm_id.and_then(|id| path_of(&state, &id).ok());
blocking(move || coa_core::msvc_setup::install_build_tools(realm_root.as_deref())).await
}
#[tauri::command]
async fn preview_register_test_realm(
state: State<'_, AppState>,
@@ -3380,6 +3390,7 @@ pub fn run() {
.invoke_handler(tauri::generate_handler![
release_history,
preview_toolchain_status,
preview_install_msvc,
preview_register_test_realm,
preview_build_module,
preview_install_module,
+6
View File
@@ -352,6 +352,10 @@ export interface ToolchainStatus {
sdk_available: boolean;
sdk_dir?: string | null;
sdk_abi_fingerprint?: string | null;
/** MSVC toolset picked for builds, its cl.exe version, and the version this core's SDK requires. */
msvc_toolset?: string | null;
msvc_cl_version?: string | null;
required_cl_version?: string | null;
ready: boolean;
diagnostic_message: string;
}
@@ -850,6 +854,8 @@ export const api = {
consoleRisk: (command: string) => invoke<"normal" | "dangerous">("console_risk", { command }),
consoleCommand: (id: string, command: string, confirmed: boolean) => invoke<string>("console_command", { id, command, confirmed }),
previewPreset: (id: string, scope: Scope, preset: string) => invoke<PresetPreview>("preview_preset", { id, scope, preset }),
previewInstallMsvc: (realmId?: string) =>
invoke<{ ready: boolean; restart_required: boolean; message: string; toolchain: ToolchainStatus }>("preview_install_msvc", { realmId: realmId ?? null }),
previewToolchainStatus: (realmId?: string) =>
invoke<ToolchainStatus>("preview_toolchain_status", { realmId: realmId ?? null }),
previewRegisterTestRealm: (path?: string) => invoke<ServerSummary>("preview_register_test_realm", { path: path ?? null }),
+42
View File
@@ -620,6 +620,8 @@ export function ModulesPage({
// Toolchain state
const [toolchain, setToolchain] = useState<ToolchainStatus | null>(null);
const [msvcInstalling, setMsvcInstalling] = useState(false);
const [msvcResult, setMsvcResult] = useState<{ ready: boolean; restart_required: boolean; message: string } | null>(null);
// Controlled Build state
const [building, setBuilding] = useState(false);
@@ -2774,7 +2776,47 @@ export function ModulesPage({
<span>Host SDK ABI Fingerprint</span>
<span className="font-mono text-muted">{toolchain.sdk_abi_fingerprint || "UNKNOWN"}</span>
</div>
<div className="flex items-center justify-between rounded border border-line p-2.5">
<span>{locale === "ru" ? "Компилятор (нужен / выбран)" : "Compiler (required / selected)"}</span>
<span className="font-mono text-muted">
{toolchain.required_cl_version ? `cl ${toolchain.required_cl_version}` : "—"} / {toolchain.msvc_cl_version ? `cl ${toolchain.msvc_cl_version} (${toolchain.msvc_toolset ?? ""})` : "NOT FOUND"}
</span>
</div>
</div>
{!toolchain.ready && (
<div className="space-y-2 rounded border border-line p-3 text-xs">
<p className="text-muted">
{locale === "ru"
? "Модули собираются на этом ПК тем же компилятором, что и ядро (MSVC 14.44). Менеджер скачает официальный установщик Visual Studio Build Tools с сайта Microsoft, проверит подпись Microsoft и откроет его с уже отмеченными компонентами. Условия Microsoft вы принимаете в окне установщика. Нужно около 4 ГБ на диске и права администратора."
: "Modules are compiled on this PC with the same compiler as the core (MSVC 14.44). The Manager downloads the official Visual Studio Build Tools installer from Microsoft, checks Microsoft's signature and opens it with the components preselected. You accept Microsoft's terms in the installer window. About 4 GB of disk space and administrator rights are needed."}
</p>
<Button
size="sm"
disabled={msvcInstalling}
onClick={async () => {
setMsvcInstalling(true);
setMsvcResult(null);
try {
const out = await api.previewInstallMsvc(serverId);
setMsvcResult(out);
setToolchain(out.toolchain);
} catch (e) {
setMsvcResult({ ready: false, restart_required: false, message: String(e) });
} finally {
setMsvcInstalling(false);
}
}}
>
{msvcInstalling
? (locale === "ru" ? "Идёт установка в окне Microsoft…" : "Installing in Microsoft's window…")
: (locale === "ru" ? "Установить Build Tools (MSVC 14.44)" : "Install Build Tools (MSVC 14.44)")}
</Button>
{msvcResult && (
<p className={msvcResult.ready ? "text-ok" : "text-warn"} role="status">{msvcResult.message}</p>
)}
</div>
)}
</Card>
)}