VS Code CMake Helper: first implementation
Build / build (pull_request) Successful in 10m29s

CMake the way CLion does it (profiles and presets, targets, run and debug
configurations, CTest), moved out of personal-assistant's cmake-extension
and renamed from CMake Targets.

- Gitea Actions: typecheck, package the .vsix, release it on a v* tag
- Updates: the latest Gitea release is looked at every 10 minutes and
  offered for install; a private repository's token in the secret storage
- cmakeHelper.toolbar: the toolbar in the status bar, the editors' title,
  or VS Code's title bar (workbench.html patched, offered again after
  VS Code or extension updates)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Tomáš ČerníkandClaude Opus 5.5 committed 2026-10-26 09:05:39 +01:00
1 parent f8032fd65d
commit fe6fd8be69
28 files changed
+9061

No files matched your search

+158
View File
@@ -0,0 +1,158 @@
/**
* The toolbar in VS Code's title bar (`cmakeHelper.toolbar`: "titleBar").
* An extension can't put anything there, so media/titlebar.js is copied next
* to VS Code's workbench.html and added to it, and product.json's checksum of
* workbench.html is updated (else VS Code says its installation is corrupt).
* A VS Code update brings a new workbench.html without it, an update of this
* extension a new script: either is noticed on start and offered to patch again.
* The installation is usually root's: then the files go in with pkexec.
*/
import * as crypto from "node:crypto";
import * as fs from "node:fs";
import * as path from "node:path";
import * as vscode from "vscode";
import { errorText, exists, run, settings } from "./util";
const SCRIPT = "cmake-helper-titlebar.js";
const HTML = "vs/code/electron-browser/workbench/workbench.html";
const MARK = "<!-- CMake Helper: the CMake toolbar in the title bar -->";
const LINE = `\t${MARK}<script src="./${SCRIPT}"></script>\n`;
type State = "absent" | "outdated" | "current" | "unsupported";
/** A file to put in place (`content`), or to remove. */
interface Change {
file: string;
content?: string;
}
export class TitleBar {
constructor(private readonly context: vscode.ExtensionContext) {}
private get files() {
const out = path.join(vscode.env.appRoot, "out");
return { html: path.join(out, HTML), script: path.join(out, path.dirname(HTML), SCRIPT), product: path.join(vscode.env.appRoot, "product.json") };
}
private get own(): string {
return fs.readFileSync(path.join(this.context.extensionPath, "media", "titlebar.js"), "utf8");
}
state(): State {
// A remote extension host has the server's installation, not the window's; the web has no workbench.html.
if (vscode.env.uiKind !== vscode.UIKind.Desktop || vscode.env.remoteName || !exists(this.files.html)) return "unsupported";
try {
if (!fs.readFileSync(this.files.html, "utf8").includes(MARK)) return "absent";
return fs.readFileSync(this.files.script, "utf8") === this.own ? "current" : "outdated";
} catch {
return "absent";
}
}
/** On start and when the setting changes: what's in VS Code against what the setting asks for. */
async reconcile(): Promise<void> {
const wanted = settings().get<string>("toolbar") === "titleBar";
const state = this.state();
if (wanted && state === "unsupported") {
void vscode.window.showWarningMessage("CMake's toolbar in the title bar works only in desktop VS Code with a local workspace; here it stays in the status bar.");
return;
}
if (wanted && state === "absent") {
const patchedFor = this.context.globalState.get<string>("titleBar.vscodeVersion");
const message = patchedFor
? `VS Code was updated (${patchedFor} → ${vscode.version}) and its title bar lost the CMake toolbar. Patch VS Code again?`
: "The CMake toolbar in the title bar needs VS Code's workbench.html patched (you may be asked for your password). Patch it now?";
const a = await vscode.window.showInformationMessage(message, "Patch VS Code", "Use Status Bar");
if (a === "Patch VS Code") await this.patch();
if (a === "Use Status Bar") await settings().update("toolbar", "statusBar", vscode.ConfigurationTarget.Global);
} else if (wanted && state === "outdated") {
const a = await vscode.window.showInformationMessage("CMake Helper was updated: update its toolbar in VS Code's title bar too?", "Update");
if (a) await this.patch();
} else if (!wanted && (state === "current" || state === "outdated")) {
const a = await vscode.window.showInformationMessage("VS Code still has the CMake toolbar patched into its title bar. Remove it?", "Remove", "Use Title Bar");
if (a === "Remove") await this.unpatch();
if (a === "Use Title Bar") await settings().update("toolbar", "titleBar", vscode.ConfigurationTarget.Global);
}
}
async patch(): Promise<void> {
if (this.state() === "unsupported") {
void vscode.window.showWarningMessage("Patching the title bar works only in desktop VS Code with a local workspace.");
return;
}
const html = fs.readFileSync(this.files.html, "utf8").replace(LINE, "");
const patched = html.replace("</html>", `${LINE}</html>`);
if (await this.write([{ file: this.files.script, content: this.own }, ...this.withChecksum(patched)])) {
await this.context.globalState.update("titleBar.vscodeVersion", vscode.version);
await this.reload("The CMake toolbar is in VS Code's title bar now: reload the window (and VS Code's other windows) to see it.");
}
}
async unpatch(): Promise<void> {
if (this.state() === "unsupported") return;
const html = fs.readFileSync(this.files.html, "utf8").replace(LINE, "");
if (await this.write([...this.withChecksum(html), { file: this.files.script }])) {
await this.context.globalState.update("titleBar.vscodeVersion", undefined);
await this.reload("The CMake toolbar is out of VS Code's title bar: reload the window to see it in the status bar again.");
}
}
/** workbench.html, and product.json with its checksum (base64 of the SHA-256, unpadded) to match. */
private withChecksum(html: string): Change[] {
const changes: Change[] = [{ file: this.files.html, content: html }];
const product = fs.readFileSync(this.files.product, "utf8");
const sum = crypto.createHash("sha256").update(html, "utf8").digest("base64").replace(/=+$/, "");
const key = new RegExp(`("${HTML.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")}"\\s*:\\s*")[^"]*"`);
if (key.test(product)) changes.push({ file: this.files.product, content: product.replace(key, `$1${sum}"`) });
return changes;
}
/** Straight in if VS Code's files are this user's; else, on Linux, as root with pkexec. */
private async write(changes: Change[]): Promise<boolean> {
try {
for (const c of changes) {
if (c.content !== undefined) fs.writeFileSync(c.file, c.content);
else fs.rmSync(c.file, { force: true });
}
return true;
} catch (err) {
const code = (err as NodeJS.ErrnoException).code;
if ((code !== "EACCES" && code !== "EPERM") || process.platform !== "linux") {
void vscode.window.showErrorMessage(`Patching VS Code failed: ${errorText(err)}`);
return false;
}
}
const dir = path.join(this.context.globalStorageUri.fsPath, "titlebar");
fs.mkdirSync(dir, { recursive: true });
const args: string[] = [];
const steps: string[] = [];
changes.forEach((c, i) => {
if (c.content !== undefined) {
const tmp = path.join(dir, String(i));
fs.writeFileSync(tmp, c.content);
args.push(tmp, c.file);
steps.push(`cp "\${${args.length - 1}}" "\${${args.length}}"`);
} else {
args.push(c.file);
steps.push(`rm -f "\${${args.length}}"`);
}
});
try {
const r = await run("pkexec", ["/bin/sh", "-c", steps.join(" && "), "sh", ...args]);
// 126: the password dialog was dismissed.
if (r.code === 126) return false;
if (r.code !== 0) throw new Error(r.stderr.trim() || `pkexec exited with ${r.code}`);
return true;
} catch (err) {
void vscode.window.showErrorMessage(`Patching VS Code failed: ${errorText(err)}`);
return false;
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
}
private async reload(message: string): Promise<void> {
const a = await vscode.window.showInformationMessage(message, "Reload Window");
if (a) void vscode.commands.executeCommand("workbench.action.reloadWindow");
}
}