vibefpmgui.window.build_outputs

The Build outputs panel: what fpm keeps in build/, running a built program, and the command that compiled a file.

  1"""The Build outputs panel: what fpm keeps in build/, running a built program,
  2and the command that compiled a file."""
  3
  4from __future__ import annotations
  5
  6import html
  7import os
  8import shutil
  9import sys
 10from pathlib import Path
 11
 12from qtpy.QtCore import QProcess
 13from qtpy.QtWidgets import (
 14    QApplication,
 15    QDialog,
 16    QHBoxLayout,
 17    QLabel,
 18    QMessageBox,
 19    QPlainTextEdit,
 20    QPushButton,
 21    QVBoxLayout,
 22)
 23
 24from .. import build_outputs as bo
 25from ..compat import join_args, quote_arg, split_args
 26from ..fpm_args import TEST_RUNNER, VALGRIND_RUNNER, split_runner
 27from ..project_tree import reveal_in_file_manager
 28from ..widgets import run_dialog
 29
 30
 31class BuildOutputsMixin:
 32    """The Build outputs panel (a part of `vibefpmgui.main_window.MainWindow`)."""
 33
 34    def _build_outputs_view(self):
 35        """The Build outputs panel."""
 36        self.outputs_view = bo.BuildOutputsView()
 37        self.outputs_view.run_requested.connect(self.run_program)
 38        self.outputs_view.run_in_terminal_requested.connect(self.run_program_in_terminal)
 39        self.outputs_view.debug_requested.connect(self.debug_named)  # (built again: see window.debugging)
 40        self.outputs_view.valgrind_requested.connect(self.run_valgrind)
 41        self.outputs_view.reveal_requested.connect(reveal_in_file_manager)
 42        self.outputs_view.delete_requested.connect(self.delete_configurations)
 43        self.outputs_view.refresh_requested.connect(self.refresh_build_outputs)
 44
 45    def _config_records(self) -> bo.ConfigRecords:
 46        return bo.ConfigRecords(self.settings, self._project_key("build_configs"))
 47
 48    def _record_build_folders(self):
 49        """Remember which build folders the finished build wrote, and with
 50        which settings, so the Build outputs tab can label them."""
 51        before, self._folder_snapshot = getattr(self, "_folder_snapshot", None), None
 52        if before is None or not self.manifest_path:
 53            return
 54        changed = bo.changed_folders(before, bo.snapshot(self.manifest_path.parent))
 55        flags = " ".join(x for x in (self.flag_edit.text().strip(),
 56                                     " ".join(split_runner(split_args(self.extra_edit.text()))[0]))
 57                         if x)
 58        if changed:
 59            self._config_records().record_build(self._build_description(), flags, changed)
 60        else:  # up to date: these settings are still the current configuration
 61            self._config_records().mark_used(self._build_description(), flags)
 62        self.refresh_build_outputs()
 63
 64    def refresh_build_outputs(self):
 65        if not self.manifest_path:
 66            self.outputs_view.set_configurations([], None)
 67            return
 68        records = self._config_records()
 69        configs = bo.configurations(self.manifest_path.parent, records.load(), records.last_used())
 70        self.outputs_view.set_configurations(configs, self.manifest_path.parent)
 71
 72    def delete_configurations(self, configs: list):
 73        if not configs:
 74            return
 75        if self.busy():
 76            QMessageBox.information(self, "Build outputs", "Wait for the running command to finish first.")
 77            return
 78        size = bo.human_size(sum(c.size for c in configs))
 79        names = "\n".join(f"• {c.label} ({bo.human_size(c.size)})" for c in configs)
 80        answer = QMessageBox.question(
 81            self, "Delete build outputs",
 82            f"Delete {len(configs)} configuration{'s' * (len(configs) != 1)} ({size})?\n\n{names}\n\n"
 83            "They are rebuilt automatically when needed.")
 84        if answer != QMessageBox.Yes:
 85            return
 86        folders = [f.path for c in configs for f in c.folders]
 87        errors = bo.delete_folders(folders)
 88        self._config_records().forget([f.name for f in folders])
 89        self.refresh_build_outputs()
 90        if errors:
 91            QMessageBox.warning(self, "Build outputs", "Some folders could not be deleted:\n" + "\n".join(errors))
 92        else:
 93            self.statusBar().showMessage(f"Deleted {size} of build outputs", 5000)
 94
 95    def run_valgrind(self, kind: str, name: str):
 96        """Build a program, test or example (``kind``) with the Build options
 97        and run it under Valgrind's memory check, through fpm (``--runner``):
 98        its report goes to the Output panel."""
 99        if self.busy() or not self.manifest_path:
100            return
101        if shutil.which("valgrind") is None:
102            QMessageBox.information(self, "Valgrind", bo.NO_VALGRIND)
103            return
104        args = self.debug_arguments(kind, [name], VALGRIND_RUNNER)  # (debug information: files and lines)
105        if args is None:
106            return
107        self._valgrind_name = name
108        self.run_fpm(args[:1], True, False, phase="valgrind", args=args)
109        if self.process is not None:
110            self.show_all_output()
111            self.statusBar().showMessage(f"Building {name}, to run it under Valgrind…")
112
113    def _after_valgrind(self, code: int, stopped: bool):
114        """Valgrind's run is over: say what it found (the report's in Output).
115        Its summary says, not the exit code: fpm's is 1 for a failed build too."""
116        name = self._valgrind_name
117        if stopped:
118            return
119        errors = bo.valgrind_errors("".join(self._captured))
120        if errors is None:
121            message = (f"{name} wasn't run under Valgrind: the build failed, or Valgrind couldn't start "
122                       "(see Build and Output)" if code else f"fpm ran nothing called {name}")
123        elif errors:
124            message = f"Valgrind found {errors} error{'s' * (errors != 1)} in {name} (see Output)"
125        elif code:
126            message = f"Valgrind: no memory errors in {name}, but it failed (exit code {code})"
127        else:
128            message = f"Valgrind: no errors in {name}"
129        self._append(f"[{message}]\n", bold=True)
130        self.statusBar().showMessage(message)
131        self.show_panel(self._content(self.docks["output"]))
132
133    def run_program_in_terminal(self, path: str):
134        """Type the command to run an already-built program (with the Program
135        args) in the Terminal panel: there it can read what you type."""
136        try:
137            args = split_args(self.runner_args_edit.text())
138        except ValueError as exc:
139            QMessageBox.warning(self, "Invalid arguments", str(exc))
140            return
141        self.show_panel(self.terminal_panel)
142        self.terminal_panel.type_command(join_args([path, *args]))
143        self.terminal_panel.focus()
144
145    def run_program(self, path: str):
146        """Run an already-built program directly (no fpm, no rebuild)."""
147        if self.busy() or not self.manifest_path:
148            return
149        try:
150            args = split_args(self.runner_args_edit.text())
151        except ValueError as exc:
152            QMessageBox.warning(self, "Invalid arguments", str(exc))
153            return
154        self.process = QProcess(self)
155        self.process.setWorkingDirectory(str(self.manifest_path.parent))
156        self.process.setProcessChannelMode(QProcess.MergedChannels)
157        self.process.readyReadStandardOutput.connect(self._read_output)
158        self.process.finished.connect(self._finished)
159        self.process.errorOccurred.connect(self._process_error)
160        self._refresh_after = False
161        self._partial = ""
162        self._esc_tail = ""
163        self.console_writer.reset()
164        self._begin_output()
165        self._held_blank = False
166        # Through the same marker wrapper as Test, so the Run tab can follow it.
167        self._phase = "direct"
168        self._marker_kind = "run"
169        self._monitoring = False
170        self._test_building = False
171        self._folder_snapshot = None
172        name = Path(path).name
173        if name.lower().endswith(".exe"):
174            name = name[:-4]
175        self._process_label = name
176        self.show_all_output()
177        self.run_monitor.begin_listing(f"{Path(path).parent.parent.name} · run directly, no rebuild")
178        self.run_monitor.start([name], self._load_durations("run"))
179        self.show_panel(self.run_monitor)
180        rel = os.path.relpath(path, self.manifest_path.parent)
181        self._append(f"\n$ cd {quote_arg(str(self.manifest_path.parent))}\n"
182                     f"$ {join_args([rel] + args)}\n", bold=True)
183        self.stop_action.setEnabled(True)
184        self._set_project_enabled(True)
185        self.statusBar().showMessage(f"Running {name}…")
186        self.process.start(sys.executable, ["-I", TEST_RUNNER, path] + args)
187
188    def show_compile_command(self, source: str):
189        """The exact compiler command fpm used for a source file."""
190        if not self.manifest_path:
191            return
192        commands, _folder = bo.read_compile_commands(self.manifest_path.parent)
193        args = commands.get(str(Path(source).resolve()))
194        name = Path(source).name
195        if args is None:
196            QMessageBox.information(
197                self, "Compile command",
198                f"No compile command for {name} in build/compile_commands.json.\n\n"
199                "That file lists the files of the most recent build; build the "
200                "project (with this file in it) and try again.")
201            return
202        dialog = QDialog(self)
203        dialog.setWindowTitle(f"Compile command — {name}")
204        layout = QVBoxLayout(dialog)
205        note = QLabel("From <code>build/compile_commands.json</code>, which fpm writes when "
206                      "it compiles something — so this is the last build that compiled files, "
207                      "which may have used other settings than the current ones. "
208                      "Run from the project folder.")
209        note.setWordWrap(True)
210        layout.addWidget(note)
211        text = QPlainTextEdit(bo.format_command(args))
212        text.setReadOnly(True)
213        text.setFont(self._mono_font())
214        text.setLineWrapMode(QPlainTextEdit.NoWrap)
215        layout.addWidget(text, 1)
216        repeated = sorted({a for a in args if a.startswith("-") and args.count(a) > 1})
217        if repeated:
218            hint = QLabel("<span style='color:gray'>fpm passes some flags more than once: "
219                          + ", ".join(f"<code>{html.escape(r)}</code> ×{args.count(r)}" for r in repeated)
220                          + " (harmless).</span>")
221            hint.setWordWrap(True)
222            layout.addWidget(hint)
223        buttons = QHBoxLayout()
224        buttons.addStretch(1)
225        copy = QPushButton("Copy")
226        copy.setToolTip("Copy as one line")
227        copy.clicked.connect(lambda: (QApplication.clipboard().setText(join_args(args)),
228                                      copy.setText("Copied")))
229        close = QPushButton("Close")
230        close.setDefault(True)
231        close.clicked.connect(dialog.accept)
232        buttons.addWidget(copy)
233        buttons.addWidget(close)
234        layout.addLayout(buttons)
235        dialog.resize(720, 420)
236        run_dialog(dialog)
class BuildOutputsMixin:
 32class BuildOutputsMixin:
 33    """The Build outputs panel (a part of `vibefpmgui.main_window.MainWindow`)."""
 34
 35    def _build_outputs_view(self):
 36        """The Build outputs panel."""
 37        self.outputs_view = bo.BuildOutputsView()
 38        self.outputs_view.run_requested.connect(self.run_program)
 39        self.outputs_view.run_in_terminal_requested.connect(self.run_program_in_terminal)
 40        self.outputs_view.debug_requested.connect(self.debug_named)  # (built again: see window.debugging)
 41        self.outputs_view.valgrind_requested.connect(self.run_valgrind)
 42        self.outputs_view.reveal_requested.connect(reveal_in_file_manager)
 43        self.outputs_view.delete_requested.connect(self.delete_configurations)
 44        self.outputs_view.refresh_requested.connect(self.refresh_build_outputs)
 45
 46    def _config_records(self) -> bo.ConfigRecords:
 47        return bo.ConfigRecords(self.settings, self._project_key("build_configs"))
 48
 49    def _record_build_folders(self):
 50        """Remember which build folders the finished build wrote, and with
 51        which settings, so the Build outputs tab can label them."""
 52        before, self._folder_snapshot = getattr(self, "_folder_snapshot", None), None
 53        if before is None or not self.manifest_path:
 54            return
 55        changed = bo.changed_folders(before, bo.snapshot(self.manifest_path.parent))
 56        flags = " ".join(x for x in (self.flag_edit.text().strip(),
 57                                     " ".join(split_runner(split_args(self.extra_edit.text()))[0]))
 58                         if x)
 59        if changed:
 60            self._config_records().record_build(self._build_description(), flags, changed)
 61        else:  # up to date: these settings are still the current configuration
 62            self._config_records().mark_used(self._build_description(), flags)
 63        self.refresh_build_outputs()
 64
 65    def refresh_build_outputs(self):
 66        if not self.manifest_path:
 67            self.outputs_view.set_configurations([], None)
 68            return
 69        records = self._config_records()
 70        configs = bo.configurations(self.manifest_path.parent, records.load(), records.last_used())
 71        self.outputs_view.set_configurations(configs, self.manifest_path.parent)
 72
 73    def delete_configurations(self, configs: list):
 74        if not configs:
 75            return
 76        if self.busy():
 77            QMessageBox.information(self, "Build outputs", "Wait for the running command to finish first.")
 78            return
 79        size = bo.human_size(sum(c.size for c in configs))
 80        names = "\n".join(f"• {c.label} ({bo.human_size(c.size)})" for c in configs)
 81        answer = QMessageBox.question(
 82            self, "Delete build outputs",
 83            f"Delete {len(configs)} configuration{'s' * (len(configs) != 1)} ({size})?\n\n{names}\n\n"
 84            "They are rebuilt automatically when needed.")
 85        if answer != QMessageBox.Yes:
 86            return
 87        folders = [f.path for c in configs for f in c.folders]
 88        errors = bo.delete_folders(folders)
 89        self._config_records().forget([f.name for f in folders])
 90        self.refresh_build_outputs()
 91        if errors:
 92            QMessageBox.warning(self, "Build outputs", "Some folders could not be deleted:\n" + "\n".join(errors))
 93        else:
 94            self.statusBar().showMessage(f"Deleted {size} of build outputs", 5000)
 95
 96    def run_valgrind(self, kind: str, name: str):
 97        """Build a program, test or example (``kind``) with the Build options
 98        and run it under Valgrind's memory check, through fpm (``--runner``):
 99        its report goes to the Output panel."""
100        if self.busy() or not self.manifest_path:
101            return
102        if shutil.which("valgrind") is None:
103            QMessageBox.information(self, "Valgrind", bo.NO_VALGRIND)
104            return
105        args = self.debug_arguments(kind, [name], VALGRIND_RUNNER)  # (debug information: files and lines)
106        if args is None:
107            return
108        self._valgrind_name = name
109        self.run_fpm(args[:1], True, False, phase="valgrind", args=args)
110        if self.process is not None:
111            self.show_all_output()
112            self.statusBar().showMessage(f"Building {name}, to run it under Valgrind…")
113
114    def _after_valgrind(self, code: int, stopped: bool):
115        """Valgrind's run is over: say what it found (the report's in Output).
116        Its summary says, not the exit code: fpm's is 1 for a failed build too."""
117        name = self._valgrind_name
118        if stopped:
119            return
120        errors = bo.valgrind_errors("".join(self._captured))
121        if errors is None:
122            message = (f"{name} wasn't run under Valgrind: the build failed, or Valgrind couldn't start "
123                       "(see Build and Output)" if code else f"fpm ran nothing called {name}")
124        elif errors:
125            message = f"Valgrind found {errors} error{'s' * (errors != 1)} in {name} (see Output)"
126        elif code:
127            message = f"Valgrind: no memory errors in {name}, but it failed (exit code {code})"
128        else:
129            message = f"Valgrind: no errors in {name}"
130        self._append(f"[{message}]\n", bold=True)
131        self.statusBar().showMessage(message)
132        self.show_panel(self._content(self.docks["output"]))
133
134    def run_program_in_terminal(self, path: str):
135        """Type the command to run an already-built program (with the Program
136        args) in the Terminal panel: there it can read what you type."""
137        try:
138            args = split_args(self.runner_args_edit.text())
139        except ValueError as exc:
140            QMessageBox.warning(self, "Invalid arguments", str(exc))
141            return
142        self.show_panel(self.terminal_panel)
143        self.terminal_panel.type_command(join_args([path, *args]))
144        self.terminal_panel.focus()
145
146    def run_program(self, path: str):
147        """Run an already-built program directly (no fpm, no rebuild)."""
148        if self.busy() or not self.manifest_path:
149            return
150        try:
151            args = split_args(self.runner_args_edit.text())
152        except ValueError as exc:
153            QMessageBox.warning(self, "Invalid arguments", str(exc))
154            return
155        self.process = QProcess(self)
156        self.process.setWorkingDirectory(str(self.manifest_path.parent))
157        self.process.setProcessChannelMode(QProcess.MergedChannels)
158        self.process.readyReadStandardOutput.connect(self._read_output)
159        self.process.finished.connect(self._finished)
160        self.process.errorOccurred.connect(self._process_error)
161        self._refresh_after = False
162        self._partial = ""
163        self._esc_tail = ""
164        self.console_writer.reset()
165        self._begin_output()
166        self._held_blank = False
167        # Through the same marker wrapper as Test, so the Run tab can follow it.
168        self._phase = "direct"
169        self._marker_kind = "run"
170        self._monitoring = False
171        self._test_building = False
172        self._folder_snapshot = None
173        name = Path(path).name
174        if name.lower().endswith(".exe"):
175            name = name[:-4]
176        self._process_label = name
177        self.show_all_output()
178        self.run_monitor.begin_listing(f"{Path(path).parent.parent.name} · run directly, no rebuild")
179        self.run_monitor.start([name], self._load_durations("run"))
180        self.show_panel(self.run_monitor)
181        rel = os.path.relpath(path, self.manifest_path.parent)
182        self._append(f"\n$ cd {quote_arg(str(self.manifest_path.parent))}\n"
183                     f"$ {join_args([rel] + args)}\n", bold=True)
184        self.stop_action.setEnabled(True)
185        self._set_project_enabled(True)
186        self.statusBar().showMessage(f"Running {name}…")
187        self.process.start(sys.executable, ["-I", TEST_RUNNER, path] + args)
188
189    def show_compile_command(self, source: str):
190        """The exact compiler command fpm used for a source file."""
191        if not self.manifest_path:
192            return
193        commands, _folder = bo.read_compile_commands(self.manifest_path.parent)
194        args = commands.get(str(Path(source).resolve()))
195        name = Path(source).name
196        if args is None:
197            QMessageBox.information(
198                self, "Compile command",
199                f"No compile command for {name} in build/compile_commands.json.\n\n"
200                "That file lists the files of the most recent build; build the "
201                "project (with this file in it) and try again.")
202            return
203        dialog = QDialog(self)
204        dialog.setWindowTitle(f"Compile command — {name}")
205        layout = QVBoxLayout(dialog)
206        note = QLabel("From <code>build/compile_commands.json</code>, which fpm writes when "
207                      "it compiles something — so this is the last build that compiled files, "
208                      "which may have used other settings than the current ones. "
209                      "Run from the project folder.")
210        note.setWordWrap(True)
211        layout.addWidget(note)
212        text = QPlainTextEdit(bo.format_command(args))
213        text.setReadOnly(True)
214        text.setFont(self._mono_font())
215        text.setLineWrapMode(QPlainTextEdit.NoWrap)
216        layout.addWidget(text, 1)
217        repeated = sorted({a for a in args if a.startswith("-") and args.count(a) > 1})
218        if repeated:
219            hint = QLabel("<span style='color:gray'>fpm passes some flags more than once: "
220                          + ", ".join(f"<code>{html.escape(r)}</code> ×{args.count(r)}" for r in repeated)
221                          + " (harmless).</span>")
222            hint.setWordWrap(True)
223            layout.addWidget(hint)
224        buttons = QHBoxLayout()
225        buttons.addStretch(1)
226        copy = QPushButton("Copy")
227        copy.setToolTip("Copy as one line")
228        copy.clicked.connect(lambda: (QApplication.clipboard().setText(join_args(args)),
229                                      copy.setText("Copied")))
230        close = QPushButton("Close")
231        close.setDefault(True)
232        close.clicked.connect(dialog.accept)
233        buttons.addWidget(copy)
234        buttons.addWidget(close)
235        layout.addLayout(buttons)
236        dialog.resize(720, 420)
237        run_dialog(dialog)

The Build outputs panel (a part of vibefpmgui.main_window.MainWindow).

def refresh_build_outputs(self):
65    def refresh_build_outputs(self):
66        if not self.manifest_path:
67            self.outputs_view.set_configurations([], None)
68            return
69        records = self._config_records()
70        configs = bo.configurations(self.manifest_path.parent, records.load(), records.last_used())
71        self.outputs_view.set_configurations(configs, self.manifest_path.parent)
def delete_configurations(self, configs: list):
73    def delete_configurations(self, configs: list):
74        if not configs:
75            return
76        if self.busy():
77            QMessageBox.information(self, "Build outputs", "Wait for the running command to finish first.")
78            return
79        size = bo.human_size(sum(c.size for c in configs))
80        names = "\n".join(f"• {c.label} ({bo.human_size(c.size)})" for c in configs)
81        answer = QMessageBox.question(
82            self, "Delete build outputs",
83            f"Delete {len(configs)} configuration{'s' * (len(configs) != 1)} ({size})?\n\n{names}\n\n"
84            "They are rebuilt automatically when needed.")
85        if answer != QMessageBox.Yes:
86            return
87        folders = [f.path for c in configs for f in c.folders]
88        errors = bo.delete_folders(folders)
89        self._config_records().forget([f.name for f in folders])
90        self.refresh_build_outputs()
91        if errors:
92            QMessageBox.warning(self, "Build outputs", "Some folders could not be deleted:\n" + "\n".join(errors))
93        else:
94            self.statusBar().showMessage(f"Deleted {size} of build outputs", 5000)
def run_valgrind(self, kind: str, name: str):
 96    def run_valgrind(self, kind: str, name: str):
 97        """Build a program, test or example (``kind``) with the Build options
 98        and run it under Valgrind's memory check, through fpm (``--runner``):
 99        its report goes to the Output panel."""
100        if self.busy() or not self.manifest_path:
101            return
102        if shutil.which("valgrind") is None:
103            QMessageBox.information(self, "Valgrind", bo.NO_VALGRIND)
104            return
105        args = self.debug_arguments(kind, [name], VALGRIND_RUNNER)  # (debug information: files and lines)
106        if args is None:
107            return
108        self._valgrind_name = name
109        self.run_fpm(args[:1], True, False, phase="valgrind", args=args)
110        if self.process is not None:
111            self.show_all_output()
112            self.statusBar().showMessage(f"Building {name}, to run it under Valgrind…")

Build a program, test or example (kind) with the Build options and run it under Valgrind's memory check, through fpm (--runner): its report goes to the Output panel.

def run_program_in_terminal(self, path: str):
134    def run_program_in_terminal(self, path: str):
135        """Type the command to run an already-built program (with the Program
136        args) in the Terminal panel: there it can read what you type."""
137        try:
138            args = split_args(self.runner_args_edit.text())
139        except ValueError as exc:
140            QMessageBox.warning(self, "Invalid arguments", str(exc))
141            return
142        self.show_panel(self.terminal_panel)
143        self.terminal_panel.type_command(join_args([path, *args]))
144        self.terminal_panel.focus()

Type the command to run an already-built program (with the Program args) in the Terminal panel: there it can read what you type.

def run_program(self, path: str):
146    def run_program(self, path: str):
147        """Run an already-built program directly (no fpm, no rebuild)."""
148        if self.busy() or not self.manifest_path:
149            return
150        try:
151            args = split_args(self.runner_args_edit.text())
152        except ValueError as exc:
153            QMessageBox.warning(self, "Invalid arguments", str(exc))
154            return
155        self.process = QProcess(self)
156        self.process.setWorkingDirectory(str(self.manifest_path.parent))
157        self.process.setProcessChannelMode(QProcess.MergedChannels)
158        self.process.readyReadStandardOutput.connect(self._read_output)
159        self.process.finished.connect(self._finished)
160        self.process.errorOccurred.connect(self._process_error)
161        self._refresh_after = False
162        self._partial = ""
163        self._esc_tail = ""
164        self.console_writer.reset()
165        self._begin_output()
166        self._held_blank = False
167        # Through the same marker wrapper as Test, so the Run tab can follow it.
168        self._phase = "direct"
169        self._marker_kind = "run"
170        self._monitoring = False
171        self._test_building = False
172        self._folder_snapshot = None
173        name = Path(path).name
174        if name.lower().endswith(".exe"):
175            name = name[:-4]
176        self._process_label = name
177        self.show_all_output()
178        self.run_monitor.begin_listing(f"{Path(path).parent.parent.name} · run directly, no rebuild")
179        self.run_monitor.start([name], self._load_durations("run"))
180        self.show_panel(self.run_monitor)
181        rel = os.path.relpath(path, self.manifest_path.parent)
182        self._append(f"\n$ cd {quote_arg(str(self.manifest_path.parent))}\n"
183                     f"$ {join_args([rel] + args)}\n", bold=True)
184        self.stop_action.setEnabled(True)
185        self._set_project_enabled(True)
186        self.statusBar().showMessage(f"Running {name}…")
187        self.process.start(sys.executable, ["-I", TEST_RUNNER, path] + args)

Run an already-built program directly (no fpm, no rebuild).

def show_compile_command(self, source: str):
189    def show_compile_command(self, source: str):
190        """The exact compiler command fpm used for a source file."""
191        if not self.manifest_path:
192            return
193        commands, _folder = bo.read_compile_commands(self.manifest_path.parent)
194        args = commands.get(str(Path(source).resolve()))
195        name = Path(source).name
196        if args is None:
197            QMessageBox.information(
198                self, "Compile command",
199                f"No compile command for {name} in build/compile_commands.json.\n\n"
200                "That file lists the files of the most recent build; build the "
201                "project (with this file in it) and try again.")
202            return
203        dialog = QDialog(self)
204        dialog.setWindowTitle(f"Compile command — {name}")
205        layout = QVBoxLayout(dialog)
206        note = QLabel("From <code>build/compile_commands.json</code>, which fpm writes when "
207                      "it compiles something — so this is the last build that compiled files, "
208                      "which may have used other settings than the current ones. "
209                      "Run from the project folder.")
210        note.setWordWrap(True)
211        layout.addWidget(note)
212        text = QPlainTextEdit(bo.format_command(args))
213        text.setReadOnly(True)
214        text.setFont(self._mono_font())
215        text.setLineWrapMode(QPlainTextEdit.NoWrap)
216        layout.addWidget(text, 1)
217        repeated = sorted({a for a in args if a.startswith("-") and args.count(a) > 1})
218        if repeated:
219            hint = QLabel("<span style='color:gray'>fpm passes some flags more than once: "
220                          + ", ".join(f"<code>{html.escape(r)}</code> ×{args.count(r)}" for r in repeated)
221                          + " (harmless).</span>")
222            hint.setWordWrap(True)
223            layout.addWidget(hint)
224        buttons = QHBoxLayout()
225        buttons.addStretch(1)
226        copy = QPushButton("Copy")
227        copy.setToolTip("Copy as one line")
228        copy.clicked.connect(lambda: (QApplication.clipboard().setText(join_args(args)),
229                                      copy.setText("Copied")))
230        close = QPushButton("Close")
231        close.setDefault(True)
232        close.clicked.connect(dialog.accept)
233        buttons.addWidget(copy)
234        buttons.addWidget(close)
235        layout.addLayout(buttons)
236        dialog.resize(720, 420)
237        run_dialog(dialog)

The exact compiler command fpm used for a source file.