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.