Just a small changes #3
No files matched your search
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "VS Code CMake Helper",
|
||||||
|
"build": { "dockerfile": "../Dockerfile", "context": ".." },
|
||||||
|
"workspaceFolder": "/workspace",
|
||||||
|
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind",
|
||||||
|
"remoteUser": "node",
|
||||||
|
"postCreateCommand": "npm ci",
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
// C/C++ for example/, and the debugger the extension's Debug uses.
|
||||||
|
"extensions": ["ms-vscode.cpptools"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules/
|
||||||
|
out/
|
||||||
|
*.vsix
|
||||||
|
.git/
|
||||||
|
example/build/
|
||||||
|
example/cmake-build-*/
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
out/
|
out/
|
||||||
*.vsix
|
*.vsix
|
||||||
|
example/build/
|
||||||
|
example/cmake-build-*/
|
||||||
Vendored
+8
@@ -8,6 +8,14 @@
|
|||||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
|
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
|
||||||
"outFiles": ["${workspaceFolder}/out/**/*.js"],
|
"outFiles": ["${workspaceFolder}/out/**/*.js"],
|
||||||
"preLaunchTask": "npm: compile"
|
"preLaunchTask": "npm: compile"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Run Extension (example)",
|
||||||
|
"type": "extensionHost",
|
||||||
|
"request": "launch",
|
||||||
|
"args": ["--extensionDevelopmentPath=${workspaceFolder}", "${workspaceFolder}/example"],
|
||||||
|
"outFiles": ["${workspaceFolder}/out/**/*.js"],
|
||||||
|
"preLaunchTask": "npm: compile"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -4,3 +4,9 @@ tsconfig.json
|
|||||||
**/*.map
|
**/*.map
|
||||||
.gitignore
|
.gitignore
|
||||||
.vscode/**
|
.vscode/**
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
.devcontainer/**
|
||||||
|
.gitea/**
|
||||||
|
scripts/**
|
||||||
|
example/**
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
# Everything the extension needs to be built and packaged (Node), and what it
|
||||||
|
# drives, to try it on example/: CMake, Ninja, a C++ compiler, GDB.
|
||||||
|
# Used by scripts/docker.sh and by the dev container (.devcontainer/).
|
||||||
|
FROM node:22-bookworm
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends cmake ninja-build make g++ gdb gdbserver git ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ENV NPM_CONFIG_UPDATE_NOTIFIER=false
|
||||||
|
WORKDIR /workspace
|
||||||
|
USER node
|
||||||
|
CMD ["bash"]
|
||||||
@@ -7,10 +7,10 @@ CMake in VS Code the way CLion does it. Standalone: it doesn't need (or want) Mi
|
|||||||
- **Profiles** (the Profiles view, the `$(layers)` item in the status bar): each one is a build directory with its own build type, generator, toolchain, cache variables and environment. They come from the `cmakeHelper.profiles` setting and from the configure presets in `CMakePresets.json` / `CMakeUserPresets.json`. With neither, there's CLion's default: **Debug** in `cmake-build-debug`. Switching profiles reads that directory's last configure if there is one, and configures otherwise.
|
- **Profiles** (the Profiles view, the `$(layers)` item in the status bar): each one is a build directory with its own build type, generator, toolchain, cache variables and environment. They come from the `cmakeHelper.profiles` setting and from the configure presets in `CMakePresets.json` / `CMakeUserPresets.json`. With neither, there's CLion's default: **Debug** in `cmake-build-debug`. Switching profiles reads that directory's last configure if there is one, and configures otherwise.
|
||||||
- **Targets** (the Targets view): executables, libraries and custom targets (`add_custom_target`; `showUtilityTargets` turns them off; CTest's dashboard targets are never listed), each with its sources in CMake's source groups. Inline buttons ▶ Run, 🐞 Debug, 🔨 Build. The menu has *Set as Active*, *Edit Run Configuration…*, *Rebuild*, *Go to Definition in CMakeLists* and *Copy Path of the Built File*.
|
- **Targets** (the Targets view): executables, libraries and custom targets (`add_custom_target`; `showUtilityTargets` turns them off; CTest's dashboard targets are never listed), each with its sources in CMake's source groups. Inline buttons ▶ Run, 🐞 Debug, 🔨 Build. The menu has *Set as Active*, *Edit Run Configuration…*, *Rebuild*, *Go to Definition in CMakeLists* and *Copy Path of the Built File*.
|
||||||
- **One active target** in the status bar: `$(target) app`, then 🔨 ▶ 🐞. Keys as in CLion: **Ctrl+F9** build, **Shift+F10** run, **Shift+F9** debug, **Alt+Shift+F10** choose the target.
|
- **One active target** in the status bar: `$(target) app`, then 🔨 ▶ 🐞. Keys as in CLion: **Ctrl+F9** build, **Shift+F10** run, **Shift+F9** debug, **Alt+Shift+F10** choose the target.
|
||||||
- **Run configurations per target**, edited as CLion's *Run/Debug Configurations* (*Edit Run Configuration…* on a target, or *Edit Configurations…* in the target picker): every target on the left, by kind; on the right its executable (the target's own, another target's, or a file: what a custom target or a library runs after it's built), program arguments, working directory (default: the executable's directory), environment (`A=1;B=2`) and *Before launch: Build*. A custom target with no executable is only built by ▶, as in CLion. They're kept in the workspace settings under `cmakeHelper.runConfigurations`.
|
- **Run configurations per target**, edited as CLion's *Run/Debug Configurations* (*Edit Run Configuration…* on a target, or *Edit Configurations…* in the target picker): every target on the left, by kind; on the right its executable (the target's own, another target's, or a file: what a custom target or a library runs after it's built), program arguments, working directory (default: the executable's directory), environment (`A=1;B=2`) and *Before launch: Build*. A custom target with no executable is only built by ▶, as in CLion; 🐞 debugs the program its command runs (`COMMAND app --flag`: `app` with `--flag`, in the command's directory, with `cmake -E env`'s variables), after building what the target depends on (not the target itself, which would run the command). The command comes from the build system, so this needs a Ninja or Makefile generator. They're kept in the workspace settings under `cmakeHelper.runConfigurations`.
|
||||||
- **Debugging** through C/C++ (`cppdbg`, or `cppvsdbg` with MSVC), CodeLLDB or LLDB DAP: the first one installed, or the one set in `cmakeHelper.debugger`. Extra launch properties go in `cmakeHelper.debugConfiguration`.
|
- **Debugging** through C/C++ (`cppdbg`, or `cppvsdbg` with MSVC), CodeLLDB or LLDB DAP: the first one installed, or the one set in `cmakeHelper.debugger`. Extra launch properties go in `cmakeHelper.debugConfiguration`.
|
||||||
- **CTest in the Test Explorer**: tests are grouped by the target whose executable they run, and by suite for names like `Suite.Test`. Each one sits at its `TEST(…)` / `TEST_CASE("…")` / `BOOST_AUTO_TEST_CASE(…)` in the sources (with ▶ in the gutter), or else at its `add_test`. Running builds the tests' targets first, then calls `ctest` and reads the results from JUnit. Debugging starts the test's command under the debugger. Tests that `gtest_discover_tests` finds only after a build show up once the build is done.
|
- **CTest in the Test Explorer**: tests are grouped by the target whose executable they run, and by suite for names like `Suite.Test`. Each one sits at its `TEST(…)` / `TEST_CASE("…")` / `BOOST_AUTO_TEST_CASE(…)` in the sources (with ▶ in the gutter), or else at its `add_test`. Running builds the tests' targets first, then calls `ctest` and reads the results from JUnit. Debugging starts the test's command under the debugger. Tests that `gtest_discover_tests` finds only after a build show up once the build is done.
|
||||||
- **Code lenses**: ▶ Run / 🐞 Debug above `main()` in an executable's sources, and Run / Debug / Build above each `add_executable` / `add_library` in CMake files.
|
- **Code lenses**: ▶ Run / 🐞 Debug above `main()` in an executable's sources, and Run / Debug / Build above each `add_executable` / `add_custom_target`, and Build above each `add_library`, in CMake files.
|
||||||
- **Load CMake Project** on a `CMakeLists.txt` (the explorer's and the editor's context menu, as in CLion): that folder becomes the project (`cmakeHelper.sourceDirectory`) and it's configured; on the project's own `CMakeLists.txt` the menu has *Reload CMake Project*.
|
- **Load CMake Project** on a `CMakeLists.txt` (the explorer's and the editor's context menu, as in CLion): that folder becomes the project (`cmakeHelper.sourceDirectory`) and it's configured; on the project's own `CMakeLists.txt` the menu has *Reload CMake Project*.
|
||||||
- **Reload**: when one of the project's CMake files changes, a notification and a `Reload CMake` item in the status bar appear (`cmakeHelper.autoReload`: `ask`, `always`, `never`). *Reset Cache and Reload Project* removes `CMakeCache.txt` and `CMakeFiles/` first. A generator mismatch offers the reset by itself.
|
- **Reload**: when one of the project's CMake files changes, a notification and a `Reload CMake` item in the status bar appear (`cmakeHelper.autoReload`: `ask`, `always`, `never`). *Reset Cache and Reload Project* removes `CMakeCache.txt` and `CMakeFiles/` first. A generator mismatch offers the reset by itself.
|
||||||
- `compile_commands.json` is exported (`exportCompileCommands`), and can be copied where clangd looks for it (`copyCompileCommands`, e.g. `${sourceDir}/compile_commands.json`).
|
- `compile_commands.json` is exported (`exportCompileCommands`), and can be copied where clangd looks for it (`copyCompileCommands`, e.g. `${sourceDir}/compile_commands.json`).
|
||||||
@@ -70,6 +70,23 @@ Updates** looks now; `cmakeHelper.updates.check` turns it off.
|
|||||||
|
|
||||||
A release is made by the CI on a `v*` tag matching the version in `package.json`.
|
A release is made by the CI on a `v*` tag matching the version in `package.json`.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
Everything runs in the image of the `Dockerfile`: Node for the extension, and
|
||||||
|
CMake, Ninja, g++ and GDB for what it drives.
|
||||||
|
|
||||||
|
- **Dev container:** open this folder in VS Code and *Reopen in Container*
|
||||||
|
(Dev Containers extension). `npm ci` runs on creation; F5 **Run Extension
|
||||||
|
(example)** opens `example/`, a small project (a library, an executable, a
|
||||||
|
CTest test) to try it on.
|
||||||
|
- **Without VS Code:** `./scripts/docker.sh <command>` runs a command in the
|
||||||
|
image, in this folder and as you, e.g. `./scripts/docker.sh npm run package --
|
||||||
|
--skip-license`; without a command it's a shell.
|
||||||
|
|
||||||
|
The title bar patch (`cmakeHelper.toolbar`: `titleBar`) can't be tried in the
|
||||||
|
container: there the extension runs remotely and VS Code's installation is
|
||||||
|
the host's.
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# A small project to try the extension on: F5 "Run Extension (example)".
|
||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
project(example CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
|
||||||
|
add_library(greet src/greet.cpp)
|
||||||
|
target_include_directories(greet PUBLIC src)
|
||||||
|
|
||||||
|
add_executable(hello src/main.cpp)
|
||||||
|
target_link_libraries(hello PRIVATE greet)
|
||||||
|
|
||||||
|
enable_testing()
|
||||||
|
add_executable(greet_test tests/greet_test.cpp)
|
||||||
|
target_link_libraries(greet_test PRIVATE greet)
|
||||||
|
add_test(NAME greet COMMAND greet_test)
|
||||||
|
|
||||||
|
# Custom targets: ▶ builds them (runs their command), 🐞 debugs the program their command runs.
|
||||||
|
add_custom_target(run_hello
|
||||||
|
COMMAND hello CMake
|
||||||
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
|
COMMENT "Running hello")
|
||||||
|
add_custom_target(run_hello_env
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E env GREETING=hi $<TARGET_FILE:hello> "with spaces"
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E echo done)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#include "greet.h"
|
||||||
|
|
||||||
|
std::string greet(const std::string& name) {
|
||||||
|
return "Hello, " + name + "!";
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
std::string greet(const std::string& name);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
#include <iostream>
|
||||||
|
#include "greet.h"
|
||||||
|
|
||||||
|
int main(int argc, char** argv) {
|
||||||
|
std::cout << greet(argc > 1 ? argv[1] : "world") << std::endl;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#include <iostream>
|
||||||
|
#include "greet.h"
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
if (greet("CMake") != "Hello, CMake!") {
|
||||||
|
std::cerr << "unexpected greeting: " << greet("CMake") << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
+2
-2
@@ -377,7 +377,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "cmakeHelper.debug",
|
"command": "cmakeHelper.debug",
|
||||||
"when": "view == cmakeHelper.targets && viewItem =~ /^target\\.executable|\\.run$/",
|
"when": "view == cmakeHelper.targets && viewItem =~ /^target\\.(executable|utility)|\\.run$/",
|
||||||
"group": "inline@2"
|
"group": "inline@2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -397,7 +397,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "cmakeHelper.debug",
|
"command": "cmakeHelper.debug",
|
||||||
"when": "view == cmakeHelper.targets && viewItem =~ /^target\\.executable|\\.run$/",
|
"when": "view == cmakeHelper.targets && viewItem =~ /^target\\.(executable|utility)|\\.run$/",
|
||||||
"group": "2_run@2"
|
"group": "2_run@2"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Executable
+10
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Runs a command in the development image, in this folder, as you (so the
|
||||||
|
# files it makes are yours): ./scripts/docker.sh npm run package
|
||||||
|
# Without a command: a shell. The image is (re)built when the Dockerfile changes.
|
||||||
|
set -e
|
||||||
|
root=$(cd "$(dirname "$0")/.." && pwd)
|
||||||
|
image=vs-code-cmake-helper-dev
|
||||||
|
docker build -q -t "$image" "$root" >/dev/null
|
||||||
|
tty=; [ -t 0 ] && tty=-it
|
||||||
|
exec docker run --rm $tty -u "$(id -u):$(id -g)" -e HOME=/tmp -v "$root:/workspace" -w /workspace "$image" "${@:-bash}"
|
||||||
+2
-1
@@ -49,7 +49,8 @@ export class LensProvider implements vscode.CodeLensProvider {
|
|||||||
const d = t.definition;
|
const d = t.definition;
|
||||||
if (!d || !samePath(d.file, doc.fileName) || d.line > doc.lineCount) continue;
|
if (!d || !samePath(d.file, doc.fileName) || d.line > doc.lineCount) continue;
|
||||||
const range = doc.lineAt(d.line - 1).range;
|
const range = doc.lineAt(d.line - 1).range;
|
||||||
if (t.type === "EXECUTABLE") {
|
// A custom target's ▶ builds it (runs its command), 🐞 debugs the program its command runs.
|
||||||
|
if (t.type === "EXECUTABLE" || t.type === "UTILITY") {
|
||||||
lenses.push(
|
lenses.push(
|
||||||
new vscode.CodeLens(range, { title: "$(play) Run", command: "cmakeHelper.run", arguments: [t.name] }),
|
new vscode.CodeLens(range, { title: "$(play) Run", command: "cmakeHelper.run", arguments: [t.name] }),
|
||||||
new vscode.CodeLens(range, { title: "$(debug-alt) Debug", command: "cmakeHelper.debug", arguments: [t.name] }),
|
new vscode.CodeLens(range, { title: "$(debug-alt) Debug", command: "cmakeHelper.debug", arguments: [t.name] }),
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
/**
|
||||||
|
* What a custom target (add_custom_target) runs, so it can be debugged: the
|
||||||
|
* File API doesn't say, the build system does. Ninja gives the command of the
|
||||||
|
* target's edge (`ninja -t commands -s`), a Makefile has it as the recipe in
|
||||||
|
* the target's build.make. Each command is "cd <dir> && <program> <args> && …";
|
||||||
|
* `cmake -E env A=B <program>` sets its environment, and CMake's own steps
|
||||||
|
* (`cmake -E echo`…) aren't what's debugged.
|
||||||
|
*/
|
||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import type { Target } from "./fileApi";
|
||||||
|
import type { CMakeProject } from "./project";
|
||||||
|
import { exists, isWindows, run, samePath, splitArgs } from "./util";
|
||||||
|
|
||||||
|
export interface CustomCommand {
|
||||||
|
program: string;
|
||||||
|
args: string[];
|
||||||
|
cwd: string;
|
||||||
|
env: Record<string, string>;
|
||||||
|
/** The program is one of the project's executables. */
|
||||||
|
ownExecutable: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The programs the target's commands run, or undefined when its generator isn't one this knows (Visual Studio, Xcode). */
|
||||||
|
export async function customCommands(project: CMakeProject, target: Target): Promise<CustomCommand[] | undefined> {
|
||||||
|
const model = project.model;
|
||||||
|
if (!model) return undefined;
|
||||||
|
let lines: string[] | undefined;
|
||||||
|
if (/Ninja/.test(model.generator)) lines = await ninjaCommands(project, target);
|
||||||
|
else if (/Makefiles/.test(model.generator)) lines = makeCommands(project, target);
|
||||||
|
if (!lines) return undefined;
|
||||||
|
const cmake = model.cache.get("CMAKE_COMMAND");
|
||||||
|
const executables = project.targets.filter((t) => t.type === "EXECUTABLE").flatMap((t) => t.artifacts);
|
||||||
|
const out: CustomCommand[] = [];
|
||||||
|
for (const line of lines) {
|
||||||
|
let cwd = model.buildDir;
|
||||||
|
for (const step of steps(line)) {
|
||||||
|
if (step[0] === "cd" && step.length === 2) {
|
||||||
|
cwd = path.resolve(cwd, step[1]!);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const c = command(step, cwd, cmake);
|
||||||
|
if (c) out.push({ ...c, ownExecutable: executables.some((e) => samePath(e, c.program)) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The edge `<dir>/CMakeFiles/<name>` (`-<config>` with Ninja Multi-Config) is the one with the target's commands. */
|
||||||
|
async function ninjaCommands(project: CMakeProject, target: Target): Promise<string[]> {
|
||||||
|
const model = project.model!;
|
||||||
|
const rel = path.relative(model.buildDir, target.buildDir).split(path.sep).join("/");
|
||||||
|
const config = /Multi-Config/.test(model.generator) ? `-${project.configuration}` : "";
|
||||||
|
const edge = `${rel ? `${rel}/` : ""}CMakeFiles/${target.name}${config}`;
|
||||||
|
const ninja = model.cache.get("CMAKE_MAKE_PROGRAM") || "ninja";
|
||||||
|
const r = await run(ninja, ["-C", model.buildDir, "-t", "commands", "-s", edge]);
|
||||||
|
return r.code === 0 ? r.stdout.split(/\r?\n/).filter((l) => l.trim()) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The recipe of `…/CMakeFiles/<name>:` in the target's build.make; its lines run in the build directory. */
|
||||||
|
function makeCommands(project: CMakeProject, target: Target): string[] {
|
||||||
|
const file = path.join(target.buildDir, "CMakeFiles", `${target.name}.dir`, "build.make");
|
||||||
|
if (!exists(file)) return [];
|
||||||
|
const cmake = project.model!.cache.get("CMAKE_COMMAND") ?? "cmake";
|
||||||
|
const header = new RegExp(`^(\\S*/)?CMakeFiles/${target.name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}:`);
|
||||||
|
const lines: string[] = [];
|
||||||
|
let inRule = false;
|
||||||
|
for (const line of fs.readFileSync(file, "utf8").split(/\r?\n/)) {
|
||||||
|
if (line.startsWith("\t")) {
|
||||||
|
if (inRule) lines.push(line.slice(1).replace(/^[@-]+/, "").replace(/\$\(CMAKE_COMMAND\)/g, cmake));
|
||||||
|
} else {
|
||||||
|
inRule = header.test(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** "a b && c d" to [["a", "b"], ["c", "d"]]. */
|
||||||
|
function steps(line: string): string[][] {
|
||||||
|
const out: string[][] = [[]];
|
||||||
|
for (const word of splitArgs(line)) {
|
||||||
|
if (word === "&&") out.push([]);
|
||||||
|
else out[out.length - 1]!.push(word);
|
||||||
|
}
|
||||||
|
return out.filter((s) => s.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A step to a program to debug: `cmake -E env` unwrapped, CMake's own steps (echo, copy…) left out. */
|
||||||
|
function command(step: string[], cwd: string, cmake: string | undefined): Omit<CustomCommand, "ownExecutable"> | undefined {
|
||||||
|
const isCMake = (p: string) => (cmake && samePath(p, cmake)) || /^cmake(\.exe)?$/i.test(path.basename(p));
|
||||||
|
const env: Record<string, string> = {};
|
||||||
|
let words = step;
|
||||||
|
if (isCMake(words[0]!) && words[1] === "-E" && words[2] === "env") {
|
||||||
|
words = words.slice(3);
|
||||||
|
while (words.length && (/^[A-Za-z_]\w*=/.test(words[0]!) || words[0]!.startsWith("--"))) {
|
||||||
|
const w = words.shift()!;
|
||||||
|
if (w === "--") break;
|
||||||
|
const i = w.indexOf("=");
|
||||||
|
if (!w.startsWith("--")) env[w.slice(0, i)] = w.slice(i + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!words.length || isCMake(words[0]!)) return undefined;
|
||||||
|
return { program: resolveProgram(words[0]!, cwd), args: words.slice(1), cwd, env };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A path as the shell would take it: relative to the directory, a bare name from PATH. */
|
||||||
|
function resolveProgram(program: string, cwd: string): string {
|
||||||
|
if (path.isAbsolute(program)) return program;
|
||||||
|
if (program.includes("/") || (isWindows && program.includes("\\"))) return path.resolve(cwd, program);
|
||||||
|
const exts = isWindows ? (process.env.PATHEXT ?? ".EXE").split(";") : [""];
|
||||||
|
for (const dir of (process.env.PATH ?? "").split(path.delimiter)) {
|
||||||
|
for (const ext of exts) {
|
||||||
|
const p = path.join(dir, program + ext);
|
||||||
|
if (dir && exists(p)) return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return program;
|
||||||
|
}
|
||||||
+45
-1
@@ -4,11 +4,13 @@
|
|||||||
* working directory, environment, build before run), kept in the setting
|
* working directory, environment, build before run), kept in the setting
|
||||||
* `cmakeHelper.runConfigurations` by target name. An executable runs itself
|
* `cmakeHelper.runConfigurations` by target name. An executable runs itself
|
||||||
* unless another is set; a custom target or a library runs the executable set
|
* unless another is set; a custom target or a library runs the executable set
|
||||||
* for it, and without one ▶ only builds it.
|
* for it, and without one ▶ only builds it, and 🐞 debugs the program the
|
||||||
|
* custom target's command runs (customTarget.ts).
|
||||||
*/
|
*/
|
||||||
import * as path from "node:path";
|
import * as path from "node:path";
|
||||||
import * as vscode from "vscode";
|
import * as vscode from "vscode";
|
||||||
import { type Target, executableOf } from "./fileApi";
|
import { type Target, executableOf } from "./fileApi";
|
||||||
|
import { type CustomCommand, customCommands } from "./customTarget";
|
||||||
import type { CMakeProject } from "./project";
|
import type { CMakeProject } from "./project";
|
||||||
import { errorText, exists, expand, isWindows, onPath, settings, splitArgs } from "./util";
|
import { errorText, exists, expand, isWindows, onPath, settings, splitArgs } from "./util";
|
||||||
|
|
||||||
@@ -105,12 +107,54 @@ export async function runTarget(project: CMakeProject, target: Target): Promise<
|
|||||||
|
|
||||||
/** 🐞 Debug: builds it (if set to), then starts a debug session on its program. */
|
/** 🐞 Debug: builds it (if set to), then starts a debug session on its program. */
|
||||||
export async function debugTarget(project: CMakeProject, target: Target): Promise<void> {
|
export async function debugTarget(project: CMakeProject, target: Target): Promise<void> {
|
||||||
|
if (!programOf(project, target) && target.type === "UTILITY") return debugCustomTarget(project, target);
|
||||||
if (!programOf(project, target)) return noProgram(target, "debug");
|
if (!programOf(project, target)) return noProgram(target, "debug");
|
||||||
if (!(await buildFirst(project, target))) return;
|
if (!(await buildFirst(project, target))) return;
|
||||||
const l = launchOf(project, target);
|
const l = launchOf(project, target);
|
||||||
if (l) await debugProgram(project, l);
|
if (l) await debugProgram(project, l);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🐞 on a custom target without an executable set: the program its command runs (the project's
|
||||||
|
* own executable, if one; else a choice), as the command runs it. What it depends on is built
|
||||||
|
* first, not the target itself: that would run the command.
|
||||||
|
*/
|
||||||
|
async function debugCustomTarget(project: CMakeProject, target: Target): Promise<void> {
|
||||||
|
const commands = await customCommands(project, target);
|
||||||
|
if (!commands) {
|
||||||
|
void vscode.window.showWarningMessage(`Debugging a custom target needs a Ninja or Makefile generator (this is ${project.model?.generator}): set an executable for ${target.name} in its run configuration.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const own = commands.filter((c) => c.ownExecutable);
|
||||||
|
let c: CustomCommand | undefined = own.length === 1 ? own[0] : commands.length === 1 ? commands[0] : undefined;
|
||||||
|
if (!c && commands.length) {
|
||||||
|
const pick = await vscode.window.showQuickPick(
|
||||||
|
(own.length ? own : commands).map((x) => ({ label: path.basename(x.program), description: x.args.join(" "), detail: x.program, command: x })),
|
||||||
|
{ title: `Debug ${target.name}: which of its commands?` },
|
||||||
|
);
|
||||||
|
if (!pick) return;
|
||||||
|
c = pick.command;
|
||||||
|
}
|
||||||
|
if (!c) return noProgram(target, "debug");
|
||||||
|
const conf = runConfiguration(target.name);
|
||||||
|
if (conf.buildBeforeRun ?? settings().get<boolean>("buildBeforeRun", true)) {
|
||||||
|
const deps = target.dependencies.map((id) => project.targets.find((t) => t.id === id)?.name).filter((n): n is string => !!n);
|
||||||
|
if (deps.length && !(await project.build(deps))) {
|
||||||
|
const action = await vscode.window.showErrorMessage(`Building what ${target.name} depends on failed.`, "Debug Anyway");
|
||||||
|
if (action !== "Debug Anyway") return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!exists(c.program)) {
|
||||||
|
void vscode.window.showErrorMessage(`${c.program} isn't there. Build what ${target.name} depends on first.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const vars = project.vars(target);
|
||||||
|
const env: Record<string, string> = { ...project.activeProfile.env, ...c.env };
|
||||||
|
for (const [k, v] of Object.entries(conf.environment ?? {})) env[k] = expand(String(v), vars);
|
||||||
|
const cwd = conf.workingDirectory ? path.resolve(project.sourceDir, expand(conf.workingDirectory, vars)) : c.cwd;
|
||||||
|
await debugProgram(project, { name: target.name, program: c.program, args: [...c.args, ...splitArgs(expand(conf.args ?? "", vars))], cwd, env });
|
||||||
|
}
|
||||||
|
|
||||||
/** Starts a debugger on a program (also a test's command). */
|
/** Starts a debugger on a program (also a test's command). */
|
||||||
export async function debugProgram(project: CMakeProject, l: Launch): Promise<boolean> {
|
export async function debugProgram(project: CMakeProject, l: Launch): Promise<boolean> {
|
||||||
const config = debugConfiguration(project, l);
|
const config = debugConfiguration(project, l);
|
||||||
|
|||||||
+4
-3
@@ -98,13 +98,14 @@ export class StatusBar implements vscode.Disposable {
|
|||||||
this.build.tooltip = busy ? "Busy…" : `Build ${t.name} (Ctrl+F9)`;
|
this.build.tooltip = busy ? "Busy…" : `Build ${t.name} (Ctrl+F9)`;
|
||||||
this.build.text = p.state === "building" ? "$(loading~spin)" : "$(tools)";
|
this.build.text = p.state === "building" ? "$(loading~spin)" : "$(tools)";
|
||||||
this.build.show();
|
this.build.show();
|
||||||
// ▶ for what has a program to run, and for a custom target (it's built); 🐞 only with a program.
|
// ▶ for what has a program to run, and for a custom target (it's built); 🐞 for those too.
|
||||||
const program = !!programOf(p, t);
|
const program = !!programOf(p, t);
|
||||||
this.run.tooltip = program ? `Run ${t.name} (Shift+F10)` : `Build ${t.name}: it runs no program (Shift+F10)`;
|
this.run.tooltip = program ? `Run ${t.name} (Shift+F10)` : `Build ${t.name}: it runs no program (Shift+F10)`;
|
||||||
this.debug.tooltip = `Debug ${t.name} (Shift+F9)`;
|
// A custom target's 🐞 debugs the program its command runs.
|
||||||
|
this.debug.tooltip = program || t.type !== "UTILITY" ? `Debug ${t.name} (Shift+F9)` : `Debug the program ${t.name}'s command runs (Shift+F9)`;
|
||||||
if (program || t.type === "UTILITY") this.run.show();
|
if (program || t.type === "UTILITY") this.run.show();
|
||||||
else this.run.hide();
|
else this.run.hide();
|
||||||
if (program) this.debug.show();
|
if (program || t.type === "UTILITY") this.debug.show();
|
||||||
else this.debug.hide();
|
else this.debug.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user