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