1 | //===- GraphWriter.cpp - Implements GraphWriter support routines ----------===// |
2 | // |
3 | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
4 | // See https://llvm.org/LICENSE.txt for license information. |
5 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
6 | // |
7 | //===----------------------------------------------------------------------===// |
8 | // |
9 | // This file implements misc. GraphWriter support routines. |
10 | // |
11 | //===----------------------------------------------------------------------===// |
12 | |
13 | #include "llvm/Support/GraphWriter.h" |
14 | |
15 | #include "DebugOptions.h" |
16 | |
17 | #include "llvm/ADT/SmallString.h" |
18 | #include "llvm/ADT/SmallVector.h" |
19 | #include "llvm/ADT/StringRef.h" |
20 | #include "llvm/Config/config.h" |
21 | #include "llvm/Support/Compiler.h" |
22 | #include "llvm/Support/ErrorHandling.h" |
23 | #include "llvm/Support/ErrorOr.h" |
24 | #include "llvm/Support/FileSystem.h" |
25 | #include "llvm/Support/Path.h" |
26 | #include "llvm/Support/Program.h" |
27 | #include "llvm/Support/raw_ostream.h" |
28 | |
29 | #ifdef __APPLE__ |
30 | #include "llvm/Support/CommandLine.h" |
31 | #include "llvm/Support/ManagedStatic.h" |
32 | #endif |
33 | |
34 | #include <string> |
35 | #include <system_error> |
36 | #include <vector> |
37 | |
38 | using namespace llvm; |
39 | |
40 | #ifdef __APPLE__ |
41 | namespace { |
42 | struct CreateViewBackground { |
43 | static void *call() { |
44 | return new cl::opt<bool>("view-background" , cl::Hidden, |
45 | cl::desc("Execute graph viewer in the background. " |
46 | "Creates tmp file litter." )); |
47 | } |
48 | }; |
49 | } // namespace |
50 | static ManagedStatic<cl::opt<bool>, CreateViewBackground> ViewBackground; |
51 | void llvm::initGraphWriterOptions() { *ViewBackground; } |
52 | #else |
53 | void llvm::initGraphWriterOptions() {} |
54 | #endif |
55 | |
56 | std::string llvm::DOT::EscapeString(const std::string &Label) { |
57 | std::string Str(Label); |
58 | for (unsigned i = 0; i != Str.length(); ++i) |
59 | switch (Str[i]) { |
60 | case '\n': |
61 | Str.insert(p: Str.begin()+i, c: '\\'); // Escape character... |
62 | ++i; |
63 | Str[i] = 'n'; |
64 | break; |
65 | case '\t': |
66 | Str.insert(p: Str.begin()+i, c: ' '); // Convert to two spaces |
67 | ++i; |
68 | Str[i] = ' '; |
69 | break; |
70 | case '\\': |
71 | if (i+1 != Str.length()) |
72 | switch (Str[i+1]) { |
73 | case 'l': continue; // don't disturb \l |
74 | case '|': case '{': case '}': |
75 | Str.erase(position: Str.begin()+i); continue; |
76 | default: break; |
77 | } |
78 | [[fallthrough]]; |
79 | case '{': case '}': |
80 | case '<': case '>': |
81 | case '|': case '"': |
82 | Str.insert(p: Str.begin()+i, c: '\\'); // Escape character... |
83 | ++i; // don't infinite loop |
84 | break; |
85 | } |
86 | return Str; |
87 | } |
88 | |
89 | /// Get a color string for this node number. Simply round-robin selects |
90 | /// from a reasonable number of colors. |
91 | StringRef llvm::DOT::getColorString(unsigned ColorNumber) { |
92 | static const int NumColors = 20; |
93 | static const char* Colors[NumColors] = { |
94 | "aaaaaa" , "aa0000" , "00aa00" , "aa5500" , "0055ff" , "aa00aa" , "00aaaa" , |
95 | "555555" , "ff5555" , "55ff55" , "ffff55" , "5555ff" , "ff55ff" , "55ffff" , |
96 | "ffaaaa" , "aaffaa" , "ffffaa" , "aaaaff" , "ffaaff" , "aaffff" }; |
97 | return Colors[ColorNumber % NumColors]; |
98 | } |
99 | |
100 | static std::string replaceIllegalFilenameChars(std::string Filename, |
101 | const char ReplacementChar) { |
102 | std::string IllegalChars = |
103 | is_style_windows(S: sys::path::Style::native) ? "\\/:?\"<>|" : "/" ; |
104 | |
105 | for (char IllegalChar : IllegalChars) { |
106 | std::replace(first: Filename.begin(), last: Filename.end(), old_value: IllegalChar, |
107 | new_value: ReplacementChar); |
108 | } |
109 | |
110 | return Filename; |
111 | } |
112 | |
113 | std::string llvm::createGraphFilename(const Twine &Name, int &FD) { |
114 | FD = -1; |
115 | SmallString<128> Filename; |
116 | |
117 | // Windows can't always handle long paths, so limit the length of the name. |
118 | std::string N = Name.str(); |
119 | if (N.size() > 140) |
120 | N.resize(n: 140); |
121 | |
122 | // Replace illegal characters in graph Filename with '_' if needed |
123 | std::string CleansedName = replaceIllegalFilenameChars(Filename: N, ReplacementChar: '_'); |
124 | |
125 | std::error_code EC = |
126 | sys::fs::createTemporaryFile(Prefix: CleansedName, Suffix: "dot" , ResultFD&: FD, ResultPath&: Filename); |
127 | if (EC) { |
128 | errs() << "Error: " << EC.message() << "\n" ; |
129 | return "" ; |
130 | } |
131 | |
132 | errs() << "Writing '" << Filename << "'... " ; |
133 | return std::string(Filename); |
134 | } |
135 | |
136 | // Execute the graph viewer. Return true if there were errors. |
137 | static bool ExecGraphViewer(StringRef ExecPath, std::vector<StringRef> &args, |
138 | StringRef Filename, bool wait, |
139 | std::string &ErrMsg) { |
140 | if (wait) { |
141 | if (sys::ExecuteAndWait(Program: ExecPath, Args: args, Env: std::nullopt, Redirects: {}, SecondsToWait: 0, MemoryLimit: 0, ErrMsg: &ErrMsg)) { |
142 | errs() << "Error: " << ErrMsg << "\n" ; |
143 | return true; |
144 | } |
145 | sys::fs::remove(path: Filename); |
146 | errs() << " done. \n" ; |
147 | } else { |
148 | sys::ExecuteNoWait(Program: ExecPath, Args: args, Env: std::nullopt, Redirects: {}, MemoryLimit: 0, ErrMsg: &ErrMsg); |
149 | errs() << "Remember to erase graph file: " << Filename << "\n" ; |
150 | } |
151 | return false; |
152 | } |
153 | |
154 | namespace { |
155 | |
156 | struct GraphSession { |
157 | std::string LogBuffer; |
158 | |
159 | bool TryFindProgram(StringRef Names, std::string &ProgramPath) { |
160 | raw_string_ostream Log(LogBuffer); |
161 | SmallVector<StringRef, 8> parts; |
162 | Names.split(A&: parts, Separator: '|'); |
163 | for (auto Name : parts) { |
164 | if (ErrorOr<std::string> P = sys::findProgramByName(Name)) { |
165 | ProgramPath = *P; |
166 | return true; |
167 | } |
168 | Log << " Tried '" << Name << "'\n" ; |
169 | } |
170 | return false; |
171 | } |
172 | }; |
173 | |
174 | } // end anonymous namespace |
175 | |
176 | static const char *getProgramName(GraphProgram::Name program) { |
177 | switch (program) { |
178 | case GraphProgram::DOT: |
179 | return "dot" ; |
180 | case GraphProgram::FDP: |
181 | return "fdp" ; |
182 | case GraphProgram::NEATO: |
183 | return "neato" ; |
184 | case GraphProgram::TWOPI: |
185 | return "twopi" ; |
186 | case GraphProgram::CIRCO: |
187 | return "circo" ; |
188 | } |
189 | llvm_unreachable("bad kind" ); |
190 | } |
191 | |
192 | bool llvm::DisplayGraph(StringRef FilenameRef, bool wait, |
193 | GraphProgram::Name program) { |
194 | std::string Filename = std::string(FilenameRef); |
195 | std::string ErrMsg; |
196 | std::string ViewerPath; |
197 | GraphSession S; |
198 | |
199 | #ifdef __APPLE__ |
200 | wait &= !*ViewBackground; |
201 | if (S.TryFindProgram("open" , ViewerPath)) { |
202 | std::vector<StringRef> args; |
203 | args.push_back(ViewerPath); |
204 | if (wait) |
205 | args.push_back("-W" ); |
206 | args.push_back(Filename); |
207 | errs() << "Trying 'open' program... " ; |
208 | if (!ExecGraphViewer(ViewerPath, args, Filename, wait, ErrMsg)) |
209 | return false; |
210 | } |
211 | #endif |
212 | if (S.TryFindProgram(Names: "xdg-open" , ProgramPath&: ViewerPath)) { |
213 | std::vector<StringRef> args; |
214 | args.push_back(x: ViewerPath); |
215 | args.push_back(x: Filename); |
216 | errs() << "Trying 'xdg-open' program... " ; |
217 | if (!ExecGraphViewer(ExecPath: ViewerPath, args, Filename, wait, ErrMsg)) |
218 | return false; |
219 | } |
220 | |
221 | // Graphviz |
222 | if (S.TryFindProgram(Names: "Graphviz" , ProgramPath&: ViewerPath)) { |
223 | std::vector<StringRef> args; |
224 | args.push_back(x: ViewerPath); |
225 | args.push_back(x: Filename); |
226 | |
227 | errs() << "Running 'Graphviz' program... " ; |
228 | return ExecGraphViewer(ExecPath: ViewerPath, args, Filename, wait, ErrMsg); |
229 | } |
230 | |
231 | // xdot |
232 | if (S.TryFindProgram(Names: "xdot|xdot.py" , ProgramPath&: ViewerPath)) { |
233 | std::vector<StringRef> args; |
234 | args.push_back(x: ViewerPath); |
235 | args.push_back(x: Filename); |
236 | |
237 | args.push_back(x: "-f" ); |
238 | args.push_back(x: getProgramName(program)); |
239 | |
240 | errs() << "Running 'xdot.py' program... " ; |
241 | return ExecGraphViewer(ExecPath: ViewerPath, args, Filename, wait, ErrMsg); |
242 | } |
243 | |
244 | enum ViewerKind { |
245 | VK_None, |
246 | VK_OSXOpen, |
247 | VK_XDGOpen, |
248 | VK_Ghostview, |
249 | VK_CmdStart |
250 | }; |
251 | ViewerKind Viewer = VK_None; |
252 | #ifdef __APPLE__ |
253 | if (!Viewer && S.TryFindProgram("open" , ViewerPath)) |
254 | Viewer = VK_OSXOpen; |
255 | #endif |
256 | if (!Viewer && S.TryFindProgram(Names: "gv" , ProgramPath&: ViewerPath)) |
257 | Viewer = VK_Ghostview; |
258 | if (!Viewer && S.TryFindProgram(Names: "xdg-open" , ProgramPath&: ViewerPath)) |
259 | Viewer = VK_XDGOpen; |
260 | #ifdef _WIN32 |
261 | if (!Viewer && S.TryFindProgram("cmd" , ViewerPath)) { |
262 | Viewer = VK_CmdStart; |
263 | } |
264 | #endif |
265 | |
266 | // PostScript or PDF graph generator + PostScript/PDF viewer |
267 | std::string GeneratorPath; |
268 | if (Viewer && |
269 | (S.TryFindProgram(Names: getProgramName(program), ProgramPath&: GeneratorPath) || |
270 | S.TryFindProgram(Names: "dot|fdp|neato|twopi|circo" , ProgramPath&: GeneratorPath))) { |
271 | std::string OutputFilename = |
272 | Filename + (Viewer == VK_CmdStart ? ".pdf" : ".ps" ); |
273 | |
274 | std::vector<StringRef> args; |
275 | args.push_back(x: GeneratorPath); |
276 | if (Viewer == VK_CmdStart) |
277 | args.push_back(x: "-Tpdf" ); |
278 | else |
279 | args.push_back(x: "-Tps" ); |
280 | args.push_back(x: "-Nfontname=Courier" ); |
281 | args.push_back(x: "-Gsize=7.5,10" ); |
282 | args.push_back(x: Filename); |
283 | args.push_back(x: "-o" ); |
284 | args.push_back(x: OutputFilename); |
285 | |
286 | errs() << "Running '" << GeneratorPath << "' program... " ; |
287 | |
288 | if (ExecGraphViewer(ExecPath: GeneratorPath, args, Filename, wait: true, ErrMsg)) |
289 | return true; |
290 | |
291 | // The lifetime of StartArg must include the call of ExecGraphViewer |
292 | // because the args are passed as vector of char*. |
293 | std::string StartArg; |
294 | |
295 | args.clear(); |
296 | args.push_back(x: ViewerPath); |
297 | switch (Viewer) { |
298 | case VK_OSXOpen: |
299 | args.push_back(x: "-W" ); |
300 | args.push_back(x: OutputFilename); |
301 | break; |
302 | case VK_XDGOpen: |
303 | wait = false; |
304 | args.push_back(x: OutputFilename); |
305 | break; |
306 | case VK_Ghostview: |
307 | args.push_back(x: "--spartan" ); |
308 | args.push_back(x: OutputFilename); |
309 | break; |
310 | case VK_CmdStart: |
311 | args.push_back(x: "/S" ); |
312 | args.push_back(x: "/C" ); |
313 | StartArg = |
314 | (StringRef("start " ) + (wait ? "/WAIT " : "" ) + OutputFilename).str(); |
315 | args.push_back(x: StartArg); |
316 | break; |
317 | case VK_None: |
318 | llvm_unreachable("Invalid viewer" ); |
319 | } |
320 | |
321 | ErrMsg.clear(); |
322 | return ExecGraphViewer(ExecPath: ViewerPath, args, Filename: OutputFilename, wait, ErrMsg); |
323 | } |
324 | |
325 | // dotty |
326 | if (S.TryFindProgram(Names: "dotty" , ProgramPath&: ViewerPath)) { |
327 | std::vector<StringRef> args; |
328 | args.push_back(x: ViewerPath); |
329 | args.push_back(x: Filename); |
330 | |
331 | // Dotty spawns another app and doesn't wait until it returns |
332 | #ifdef _WIN32 |
333 | wait = false; |
334 | #endif |
335 | errs() << "Running 'dotty' program... " ; |
336 | return ExecGraphViewer(ExecPath: ViewerPath, args, Filename, wait, ErrMsg); |
337 | } |
338 | |
339 | errs() << "Error: Couldn't find a usable graph viewer program:\n" ; |
340 | errs() << S.LogBuffer << "\n" ; |
341 | return true; |
342 | } |
343 | |