util/system-util.spice
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | // Copyright (c) 2021-2026 ChilliBits. All rights reserved. | ||
| 2 | |||
| 3 | // Std imports | ||
| 4 | import "std/os/os"; | ||
| 5 | import "std/data/vector"; | ||
| 6 | import "std/os/process"; | ||
| 7 | import "std/text/analysis"; | ||
| 8 | import "std/text/stringstream"; | ||
| 9 | import "std/os/env"; | ||
| 10 | import "std/io/file"; | ||
| 11 | import "std/io/filepath"; | ||
| 12 | import "std/iterator/array-iterator"; | ||
| 13 | |||
| 14 | // Own imports | ||
| 15 | import "@/exception/fatal-error"; | ||
| 16 | import "@/driver"; | ||
| 17 | import "@/exception/compiler-error"; | ||
| 18 | import "@/exception/linker-error"; | ||
| 19 | import "@/util/test-util"; | ||
| 20 | |||
| 21 | 482 | public type ExecResult struct { | |
| 22 | public String output | ||
| 23 | public int exitCode | ||
| 24 | } | ||
| 25 | |||
| 26 | 1404 | public type ExternalBinaryFinderResult struct { | |
| 27 | public string name | ||
| 28 | public String path | ||
| 29 | } | ||
| 30 | |||
| 31 | /** | ||
| 32 | * Execute an external program without involving a shell. | ||
| 33 | * | ||
| 34 | * Each argument is passed verbatim to the program (via std/os/process), so file paths and other arguments can never be | ||
| 35 | * interpreted as shell syntax. This is the safe variant that must be used for any command whose arguments are | ||
| 36 | * influenced by (untrusted) user input, e.g. the linker invocation. | ||
| 37 | * | ||
| 38 | * @param program Name or path of the program to execute (resolved against PATH if it is a bare name) | ||
| 39 | * @param args Arguments passed to the program (without argv[0], which is added automatically) | ||
| 40 | * @param redirectStdErrToStdOut Capture stderr into the returned output alongside stdout | ||
| 41 | * @return Result struct with the captured output and the process exit code | ||
| 42 | */ | ||
| 43 | 467 | public f<ExecResult> exec(const String& program, const Vector<String>& args, bool redirectStdErrToStdOut = false) { | |
| 44 | 467 | Result<ProcessResult> processResult = runProcessCaptured(program, args, redirectStdErrToStdOut); | |
| 45 | 467 | if processResult.isErr() { | |
| 46 | ✗ | const String message = "Failed to execute '" + program + "'"; | |
| 47 | ✗ | const CompilerError error = CompilerError(CompilerErrorType::IO_ERROR, message.getRaw()); | |
| 48 | ✗ | abortWithError(error.getMessage()); | |
| 49 | ✗ | } | |
| 50 | 467 | const ProcessResult& process = processResult.unwrap(); | |
| 51 | 934 | return ExecResult{process.output, process.exitCode}; | |
| 52 | 467 | } | |
| 53 | |||
| 54 | /** | ||
| 55 | * Execute an external binary, inheriting the parent's standard streams. | ||
| 56 | * Unlike exec(), this does not capture the child's output: its stdin, stdout and stderr are connected directly to ours, | ||
| 57 | * so the program behaves as if invoked from the terminal. | ||
| 58 | * | ||
| 59 | * @param executablePath Path to the executable to run | ||
| 60 | * @return Exit code of the executed binary | ||
| 61 | */ | ||
| 62 | ✗ | public f<int> run(const String& executablePath) { | |
| 63 | ✗ | const Vector<String> noArgs; | |
| 64 | ✗ | Result<int> exitCode = runProcess(executablePath, noArgs); | |
| 65 | ✗ | if exitCode.isErr() { | |
| 66 | ✗ | const String message = "Failed to execute: " + executablePath; | |
| 67 | ✗ | const CompilerError error = CompilerError(CompilerErrorType::IO_ERROR, message.getRaw()); | |
| 68 | ✗ | abortWithError(error.getMessage()); | |
| 69 | ✗ | } | |
| 70 | ✗ | return exitCode.unwrap(); | |
| 71 | ✗ | } | |
| 72 | |||
| 73 | /** | ||
| 74 | * Render a program invocation as a human-readable command string. Used for debug output and error messages only - the | ||
| 75 | * actual execution happens via an argument vector (no shell), so this rendering is never executed. | ||
| 76 | * | ||
| 77 | * @param program Program to execute | ||
| 78 | * @param args Arguments for the program | ||
| 79 | * @return Command string | ||
| 80 | */ | ||
| 81 | ✗ | public f<String> renderCommandForDisplay(const String& program, const Vector<String>& args) { | |
| 82 | ✗ | StringStream command; | |
| 83 | ✗ | command << program; | |
| 84 | ✗ | foreach const String& arg : args { | |
| 85 | ✗ | command << " " << arg; | |
| 86 | ✗ | } | |
| 87 | ✗ | return command.str(); | |
| 88 | ✗ | } | |
| 89 | |||
| 90 | /** | ||
| 91 | * Expand a linker flag that may contain environment-variable references ('$VAR' on Unix, '%VAR%' on Windows) or a | ||
| 92 | * backtick-quoted command substitution (e.g. "`pkg-config --cflags --libs libcurl`"), mirroring what a shell would have | ||
| 93 | * done for these flags. Bindings rely on both forms (see e.g. std/bindings/llvm/linker-flags.spice and | ||
| 94 | * std/bindings/openssl/openssl.spice). | ||
| 95 | * | ||
| 96 | * Unlike a shell, only these two specific substitutions are recognized, and the backtick-quoted command is executed | ||
| 97 | * directly (argv-based, whitespace-split) rather than through a shell - no other shell metacharacters (';', '&&', pipes, | ||
| 98 | * redirects, quoting, globbing, ...) are interpreted, so this cannot be used to inject additional commands. | ||
| 99 | * | ||
| 100 | * A plain env-var reference can itself expand to several whitespace-separated flags (e.g. LLVM_INCLUDE_DIRS is set to | ||
| 101 | * "-I<dir1> -I<dir2>" by CMake/CI). Since there is no shell to word-split the expanded text, it is split explicitly, so | ||
| 102 | * each flag becomes its own argv entry. | ||
| 103 | * | ||
| 104 | * A pkg-config command substitution of a static link queries the static flags ('pkg-config --static'). Only those | ||
| 105 | * include the private dependencies of a library, that its static archive needs (e.g. zlib, zstd and jitterentropy for the | ||
| 106 | * libcrypto of some distributions), whereas a shared library brings them along itself. | ||
| 107 | * | ||
| 108 | * @param flag Raw linker flag, as configured via a 'core.[os.]linker.flag' attribute | ||
| 109 | * @param staticLinking Whether the flag is expanded for a static link | ||
| 110 | * @return One or more expanded flags (an expansion can result in several whitespace-separated flags) | ||
| 111 | */ | ||
| 112 | 1857 | public f<Vector<String>> expandLinkerFlag(const String& flag, bool staticLinking = false) { | |
| 113 | // Expand environment variable references within the flag ('$VAR' on Unix, '%VAR%' on Windows) | ||
| 114 | 1857 | String expanded; | |
| 115 | 1857 | unsigned long i = 0l; | |
| 116 | 21286 | while i < flag.getLength() { | |
| 117 | 19429 | const char c = flag[i]; | |
| 118 | 19429 | if isWindows() && c == '%' { | |
| 119 | ✗ | const long end = flag.find('%', i + 1l); | |
| 120 | ✗ | if end >= 0l { | |
| 121 | ✗ | const String varName = flag.getSubstring(i + 1l, cast<long>(cast<unsigned long>(end) - i - 1l)); | |
| 122 | ✗ | Result<string> value = getEnv(varName.getRaw()); | |
| 123 | ✗ | if value.isOk() { | |
| 124 | ✗ | expanded += value.unwrap(); | |
| 125 | ✗ | } | |
| 126 | ✗ | i = cast<unsigned long>(end) + 1l; | |
| 127 | ✗ | continue; | |
| 128 | ✗ | } | |
| 129 | 19429 | } else if !isWindows() && c == '$' { | |
| 130 | 9 | unsigned long j = i + 1l; | |
| 131 | 160 | while j < flag.getLength() && isAlphaNumUnderscore(flag[j]) { | |
| 132 | 151 | j++; | |
| 133 | 151 | } | |
| 134 | 9 | if j > i + 1l { | |
| 135 | 9 | const String varName = flag.getSubstring(i + 1l, cast<long>(j - i - 1l)); | |
| 136 | 9 | Result<string> value = getEnv(varName.getRaw()); | |
| 137 | 9 | if value.isOk() { | |
| 138 | 9 | expanded += value.unwrap(); | |
| 139 | 9 | } | |
| 140 | 9 | i = j; | |
| 141 | 9 | continue; | |
| 142 | 9 | } | |
| 143 | 19420 | } | |
| 144 | 19420 | expanded += c; | |
| 145 | 19420 | i++; | |
| 146 | 19420 | } | |
| 147 | |||
| 148 | // Backtick command substitution: recognized when the *entire* (env-expanded) flag is a single backtick-quoted command, | ||
| 149 | // matching existing std bindings usage. The command is split on whitespace and executed directly (no shell), so shell | ||
| 150 | // metacharacters inside the backticks are not interpreted. | ||
| 151 | 1857 | if expanded.getLength() >= 2l && expanded[0l] == '`' && expanded[expanded.getLength() - 1l] == '`' { | |
| 152 | ✗ | const String innerCommand = expanded.getSubstring(1l, cast<long>(expanded.getLength() - 2l)); | |
| 153 | ✗ | Vector<String> tokens = splitAtWhitespace(innerCommand); | |
| 154 | ✗ | if tokens.isEmpty() { | |
| 155 | ✗ | return Vector<String>(); | |
| 156 | } | ||
| 157 | ✗ | const String program = tokens.front(); | |
| 158 | ✗ | const Vector<String> cmdArgs = getLinkerFlagCommandArgs(tokens, staticLinking); | |
| 159 | ✗ | const ExecResult cmdResult = exec(program, cmdArgs); | |
| 160 | − | if cmdResult.exitCode != 0 { // LCOV_EXCL_LINE | |
| 161 | − | const String message = "Command substitution for linker flag failed: " + innerCommand; // LCOV_EXCL_LINE | |
| 162 | − | const LinkerError error = LinkerError(LinkerErrorType::LINKER_ERROR, message.getRaw()); // LCOV_EXCL_LINE | |
| 163 | − | abortWithError(error.getMessage()); // LCOV_EXCL_LINE | |
| 164 | − | } // LCOV_EXCL_LINE | |
| 165 | ✗ | expanded = cmdResult.output; | |
| 166 | ✗ | } | |
| 167 | |||
| 168 | // Word-split the (possibly command-substituted) text on whitespace, so an expansion that contains several flags (e.g. a | ||
| 169 | // multi-directory LLVM_INCLUDE_DIRS, or a pkg-config command's output) ends up as separate argv entries. | ||
| 170 | 3714 | return splitAtWhitespace(expanded); | |
| 171 | 1857 | } | |
| 172 | |||
| 173 | /** | ||
| 174 | * Get the arguments for the command of a backtick-quoted linker flag, without the program itself | ||
| 175 | * | ||
| 176 | * @param tokens Whitespace-split command, starting with the program | ||
| 177 | * @param staticLinking Whether the flag is expanded for a static link | ||
| 178 | * @return Command arguments | ||
| 179 | */ | ||
| 180 | 3 | f<Vector<String>> getLinkerFlagCommandArgs(const Vector<String>& tokens, bool staticLinking) { | |
| 181 | 3 | Vector<String> cmdArgs; | |
| 182 | // A static link needs the private dependencies of the libraries as well | ||
| 183 | 3 | if staticLinking && tokens.front() == "pkg-config" { | |
| 184 | 1 | cmdArgs.pushBack(String("--static")); | |
| 185 | 1 | } | |
| 186 | 10 | for unsigned long k = 1l; k < tokens.getSize(); k++ { | |
| 187 | 7 | cmdArgs.pushBack(tokens.get(k)); | |
| 188 | 7 | } | |
| 189 | 3 | return cmdArgs; | |
| 190 | } | ||
| 191 | |||
| 192 | /** | ||
| 193 | * Split a string at whitespace, dropping empty parts | ||
| 194 | * | ||
| 195 | * @param input Input string | ||
| 196 | * @return Non-empty parts | ||
| 197 | */ | ||
| 198 | 1857 | f<Vector<String>> splitAtWhitespace(const String& input) { | |
| 199 | 1857 | Vector<String> parts; | |
| 200 | 1857 | String current; | |
| 201 | 22180 | for unsigned long i = 0l; i < input.getLength(); i++ { | |
| 202 | 20323 | const char c = input[i]; | |
| 203 | 20323 | if c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\v' || c == '\f' { | |
| 204 | 11 | if !current.isEmpty() { | |
| 205 | 11 | parts.pushBack(current); | |
| 206 | 11 | current = String(); | |
| 207 | 11 | } | |
| 208 | 11 | } else { | |
| 209 | 20312 | current += c; | |
| 210 | 20312 | } | |
| 211 | 20323 | } | |
| 212 | 1857 | if !current.isEmpty() { | |
| 213 | 1857 | parts.pushBack(current); | |
| 214 | 1857 | } | |
| 215 | 3714 | return parts; | |
| 216 | 1857 | } | |
| 217 | |||
| 218 | /** | ||
| 219 | * Checks if a certain command is available on the computer | ||
| 220 | * | ||
| 221 | * @param cmd Command to search for | ||
| 222 | * @return Present or not | ||
| 223 | */ | ||
| 224 | 4 | public f<bool> isCommandAvailable(const string cmd) { | |
| 225 | // Look the program up on PATH without spawning a shell ('which'/'where') | ||
| 226 | 4 | const String programPath = findProgram(String(cmd)); | |
| 227 | 8 | return !programPath.isEmpty(); | |
| 228 | 4 | } | |
| 229 | |||
| 230 | /** | ||
| 231 | * Checks if Graphviz is installed on the system | ||
| 232 | * | ||
| 233 | * @return Present or not | ||
| 234 | */ | ||
| 235 | 1 | public f<bool> isGraphvizInstalled() { | |
| 236 | 1 | return isCommandAvailable("dot"); | |
| 237 | } | ||
| 238 | |||
| 239 | /** | ||
| 240 | * Search for a supported linker invoker on the system and return the executable name or path. | ||
| 241 | * This function may throw a LinkerError if no linker invoker is found. | ||
| 242 | * | ||
| 243 | * @return Name of path to the linker invoker executable | ||
| 244 | */ | ||
| 245 | 932 | public f<ExternalBinaryFinderResult> findLinkerInvoker() { | |
| 246 | 932 | foreach const string linkerInvoker : ["clang", "gcc"] { | |
| 247 | 932 | if isUnixBased() { | |
| 248 | 932 | foreach const string path : ["/usr/bin/", "/usr/local/bin/", "/bin/"] { | |
| 249 | 932 | const String linkerInvokerPath = path + linkerInvoker; | |
| 250 | 932 | if fileExists(linkerInvokerPath.getRaw()) { | |
| 251 | 1864 | return ExternalBinaryFinderResult{linkerInvoker, linkerInvokerPath}; | |
| 252 | } | ||
| 253 | 932 | } | |
| 254 | − | } else if isWindows() { // LCOV_EXCL_LINE - coverage runs on Linux | |
| 255 | − | if isCommandAvailable(linkerInvoker) { // LCOV_EXCL_LINE - coverage runs on Linux | |
| 256 | − | return ExternalBinaryFinderResult{linkerInvoker, String(linkerInvoker)}; // LCOV_EXCL_LINE - coverage runs on Linux | |
| 257 | } // LCOV_EXCL_LINE - coverage runs on Linux | ||
| 258 | − | } else { // LCOV_EXCL_LINE | |
| 259 | − | panic(Error("Unsupported platform")); // LCOV_EXCL_LINE | |
| 260 | − | } // LCOV_EXCL_LINE | |
| 261 | ✗ | } | |
| 262 | ✗ | const LinkerError error = LinkerError(LinkerErrorType::LINKER_INVOKER_NOT_FOUND, "No supported linker invoker was found on the system. Supported are: clang and gcc"); | |
| 263 | ✗ | abortWithError(error.getMessage()); | |
| 264 | ✗ | return ExternalBinaryFinderResult{"", String()}; // Unreachable, abortWithError exits the process | |
| 265 | ✗ | } | |
| 266 | |||
| 267 | /** | ||
| 268 | * Search for a supported linker on the system and return the executable name or path. | ||
| 269 | * This function may throw a LinkerError if no linker is found. | ||
| 270 | * | ||
| 271 | * @return Name of path to the linker executable | ||
| 272 | */ | ||
| 273 | 471 | public f<ExternalBinaryFinderResult> findLinker(const CliOptions& cliOptions) { | |
| 274 | 471 | if isUnixBased() { | |
| 275 | // Use the flavor-specific lld driver, since the generic 'lld' driver refuses to be invoked directly | ||
| 276 | 471 | foreach const string linkerName : ["mold", "ld.lld", "ld64.lld", "gold", "ld"] { | |
| 277 | // mold does only support linking for unix and darwin | ||
| 278 | 471 | if linkerName == "mold" && cliOptions.targetTriple.isOSWindows() { | |
| 279 | ✗ | continue; | |
| 280 | } | ||
| 281 | 942 | foreach const string path : ["/usr/bin/", "/usr/local/bin/", "/bin/"] { | |
| 282 | 942 | String linkerPath = path + linkerName; | |
| 283 | 942 | if fileExists(linkerPath.getRaw()) { | |
| 284 | 942 | return ExternalBinaryFinderResult{linkerName, linkerPath}; | |
| 285 | } | ||
| 286 | 942 | } | |
| 287 | ✗ | } | |
| 288 | − | } else if isWindows() { // LCOV_EXCL_LINE - coverage runs on Linux | |
| 289 | − | foreach const string linkerName : ["lld", "ld"] { // LCOV_EXCL_LINE - coverage runs on Linux | |
| 290 | − | if isCommandAvailable(linkerName) { // LCOV_EXCL_LINE - coverage runs on Linux | |
| 291 | − | return ExternalBinaryFinderResult{linkerName, String(linkerName)}; // LCOV_EXCL_LINE - coverage runs on Linux | |
| 292 | } // LCOV_EXCL_LINE - coverage runs on Linux | ||
| 293 | − | } // LCOV_EXCL_LINE - coverage runs on Linux | |
| 294 | − | } else { // LCOV_EXCL_LINE | |
| 295 | − | panic(Error("Unsupported platform")); // LCOV_EXCL_LINE | |
| 296 | − | } // LCOV_EXCL_LINE | |
| 297 | ✗ | const LinkerError error = LinkerError(LinkerErrorType::LINKER_NOT_FOUND, "No supported linker was found on the system. Supported are: mold, lld, gold and ld"); | |
| 298 | ✗ | abortWithError(error.getMessage()); | |
| 299 | ✗ | return ExternalBinaryFinderResult{"", String()}; // Unreachable, abortWithError exits the process | |
| 300 | ✗ | } | |
| 301 | |||
| 302 | /** | ||
| 303 | * Search for a supported archiver on the system and return the executable name or path. | ||
| 304 | * This function may throw a LinkerError if no archiver is found. | ||
| 305 | * | ||
| 306 | * @return Name or path to the archiver executable | ||
| 307 | */ | ||
| 308 | 1 | public f<ExternalBinaryFinderResult> findArchiver() { | |
| 309 | 1 | if isUnixBased() { | |
| 310 | 2 | foreach const string archiverName : ["llvm-ar", "gcc-ar", "ar"] { | |
| 311 | 5 | foreach const string path : ["/usr/bin/", "/usr/local/bin/", "/bin/"] { | |
| 312 | 4 | String archiverPath = path + archiverName; | |
| 313 | 4 | if fileExists(archiverPath.getRaw()) { | |
| 314 | 2 | return ExternalBinaryFinderResult{archiverName, archiverPath}; | |
| 315 | } | ||
| 316 | 4 | } | |
| 317 | 1 | } | |
| 318 | − | } else if isWindows() { // LCOV_EXCL_LINE - coverage runs on Linux | |
| 319 | − | foreach const string archiverName : ["llvm-lib", "lib"] { // LCOV_EXCL_LINE - coverage runs on Linux | |
| 320 | − | if isCommandAvailable(archiverName) { // LCOV_EXCL_LINE - coverage runs on Linux | |
| 321 | − | return ExternalBinaryFinderResult{archiverName, String(archiverName)}; // LCOV_EXCL_LINE - coverage runs on Linux | |
| 322 | } // LCOV_EXCL_LINE - coverage runs on Linux | ||
| 323 | − | } // LCOV_EXCL_LINE - coverage runs on Linux | |
| 324 | − | } else { // LCOV_EXCL_LINE | |
| 325 | − | panic(Error("Unsupported platform")); // LCOV_EXCL_LINE | |
| 326 | − | } // LCOV_EXCL_LINE | |
| 327 | ✗ | const LinkerError error = LinkerError(LinkerErrorType::ARCHIVER_NOT_FOUND, "No supported archiver was found on the system. Supported are: llvm-ar and ar"); | |
| 328 | ✗ | abortWithError(error.getMessage()); | |
| 329 | ✗ | return ExternalBinaryFinderResult{"", String()}; // Unreachable, abortWithError exits the process | |
| 330 | ✗ | } | |
| 331 | |||
| 332 | /** | ||
| 333 | * Retrieve the dir, where the standard library lives, without failing if it is not installed. | ||
| 334 | * | ||
| 335 | * @return Std directory, or an empty path if the std was not found | ||
| 336 | */ | ||
| 337 | 5379 | public f<FilePath> findStdDir() { | |
| 338 | // Check the candidates in locals: assigning the result variable twice would not destruct its first value | ||
| 339 | 5379 | if isUnixBased() { | |
| 340 | 5379 | const FilePath installedStdPath = FilePath("/usr/lib/spice/std/"); | |
| 341 | 5379 | if installedStdPath.exists() { return installedStdPath; } | |
| 342 | 5379 | } | |
| 343 | 5379 | Result<string> value = getEnv("SPICE_STD_DIR"); | |
| 344 | 5379 | if value.isOk() { | |
| 345 | 5379 | const FilePath envStdPath = FilePath(value.unwrap()); | |
| 346 | 5379 | if envStdPath.exists() { return envStdPath; } | |
| 347 | ✗ | } | |
| 348 | ✗ | return FilePath(); | |
| 349 | } | ||
| 350 | |||
| 351 | /** | ||
| 352 | * Retrieve the dir, where the standard library lives. | ||
| 353 | * | ||
| 354 | * @return Std directory | ||
| 355 | */ | ||
| 356 | 3873 | public f<FilePath> getStdDir() { | |
| 357 | 3873 | result = findStdDir(); | |
| 358 | 3873 | if result.isEmpty() { | |
| 359 | ✗ | const CompilerError error = CompilerError(CompilerErrorType::STD_NOT_FOUND, "Standard library could not be found. Check if the env var SPICE_STD_DIR exists"); | |
| 360 | ✗ | abortWithError(error.getMessage()); | |
| 361 | ✗ | } | |
| 362 | 3873 | } | |
| 363 | |||
| 364 | /** | ||
| 365 | * Retrieve the dir, where the std ships the static support libraries that Spice programs link against - currently only the | ||
| 366 | * vendored libbacktrace. Missing rather than found is a perfectly normal outcome, hence an empty path instead of a panic. | ||
| 367 | * | ||
| 368 | * @return Std runtime library directory, or an empty path if there is none | ||
| 369 | */ | ||
| 370 | 471 | public f<FilePath> getStdRuntimeLibDir() { | |
| 371 | 471 | const FilePath stdDir = findStdDir(); | |
| 372 | 471 | if stdDir.isEmpty() { | |
| 373 | ✗ | return FilePath(); | |
| 374 | } | ||
| 375 | 471 | result = stdDir / "runtime" / "lib"; | |
| 376 | 471 | if !result.exists() { | |
| 377 | ✗ | return FilePath(); | |
| 378 | } | ||
| 379 | 471 | } | |
| 380 | |||
| 381 | /** | ||
| 382 | * Build the flags for the std TPDE bindings (std/bindings/tpde) from the TPDE headers and static libraries, that the Linux | ||
| 383 | * release packages ship with the std. Returns an empty string if the given std does not ship them. | ||
| 384 | * | ||
| 385 | * @param stdDir Std directory | ||
| 386 | * @return Value for the TPDE_FLAGS env var, or an empty string | ||
| 387 | */ | ||
| 388 | 1038 | public f<String> getStdTPDEFlags(const FilePath& stdDir) { | |
| 389 | 1038 | const FilePath tpdeDir = stdDir / "bindings" / "tpde"; | |
| 390 | 1038 | const FilePath includeDir = tpdeDir / "include"; | |
| 391 | 1038 | const FilePath libDir = tpdeDir / "lib"; | |
| 392 | 1038 | FilePath headerPath = includeDir / "tpde-llvm" / "LLVMCompiler.hpp"; | |
| 393 | 1038 | FilePath tpdeLlvmLibPath = libDir / "libtpde_llvm.a"; | |
| 394 | 1038 | if !headerPath.exists() || !tpdeLlvmLibPath.exists() { | |
| 395 | 4 | return String(); | |
| 396 | } | ||
| 397 | // The libraries reference each other, so they go into a group, that the linker rescans until all references are resolved | ||
| 398 | 1036 | result = String("-I") + includeDir.toString() + " -Wl,--start-group"; | |
| 399 | 6216 | foreach const string libName : ["libtpde_llvm.a", "libtpde.a", "libfadec.a", "libdisarm64.a", "libspdlog.a"] { | |
| 400 | 5180 | FilePath libPath = libDir / libName; | |
| 401 | 5180 | if libPath.exists() { | |
| 402 | 5178 | result += " "; | |
| 403 | 5178 | result += libPath.toString(); | |
| 404 | 5178 | } | |
| 405 | 5180 | } | |
| 406 | 1036 | result += " -Wl,--end-group"; | |
| 407 | 1038 | } | |
| 408 | |||
| 409 | /** | ||
| 410 | * Point the std TPDE bindings to the TPDE headers and libraries, that the std ships, unless TPDE_FLAGS is set already. | ||
| 411 | * The bindings only use TPDE on Linux, and the shipped libraries are built for the host machine, so this only applies to native | ||
| 412 | * builds on Linux. Has to be called before the compilation starts, because it changes the process environment. | ||
| 413 | * | ||
| 414 | * @param cliOptions Command line options | ||
| 415 | */ | ||
| 416 | 1052 | public p exportStdTPDEFlags(const CliOptions& cliOptions) { | |
| 417 | 1052 | if !isLinux() || !cliOptions.isNativeTarget { | |
| 418 | 17 | return; | |
| 419 | } | ||
| 420 | 1035 | const Result<string> existingFlags = getEnv("TPDE_FLAGS"); | |
| 421 | 1035 | if existingFlags.isOk() { | |
| 422 | ✗ | return; | |
| 423 | } | ||
| 424 | 1035 | const FilePath stdDir = findStdDir(); | |
| 425 | 1035 | if stdDir.isEmpty() { | |
| 426 | ✗ | return; | |
| 427 | } | ||
| 428 | 1035 | const String flags = getStdTPDEFlags(stdDir); | |
| 429 | 1035 | if !flags.isEmpty() { | |
| 430 | 1035 | const bool exported = setEnv("TPDE_FLAGS", flags.getRaw()); | |
| 431 | 1035 | assert exported; | |
| 432 | 1035 | } | |
| 433 | 1035 | } | |
| 434 | |||
| 435 | /** | ||
| 436 | * Check if the given target triple has a Darwin based OS. Mirrors llvm::Triple::isOSDarwin(): macOS, iOS (incl. tvOS), | ||
| 437 | * watchOS, DriverKit, visionOS, bridgeOS and Apple firmware. | ||
| 438 | * | ||
| 439 | * @param triple Target triple | ||
| 440 | * @return Darwin based or not | ||
| 441 | */ | ||
| 442 | 3245 | f<bool> isDarwinTriple(const String& triple) { | |
| 443 | 3245 | const string[9] darwinOsNames = ["darwin", "macos", "ios", "tvos", "watchos", "driverkit", "xros", "visionos", "bridgeos"]; | |
| 444 | 32200 | for unsigned int i = 0; i < len(darwinOsNames); i++ { | |
| 445 | 28988 | if triple.contains(darwinOsNames[i]) { | |
| 446 | 33 | return true; | |
| 447 | } | ||
| 448 | 28955 | } | |
| 449 | 3212 | return triple.contains("apple") && triple.contains("firmware"); | |
| 450 | } | ||
| 451 | |||
| 452 | /** | ||
| 453 | * Retrieve the file extension of the output file for a certain output container. It depends on the target, which is derived | ||
| 454 | * from the target triple. As long as there is none yet, e.g. because the cli options are not enriched yet, the host machine is assumed. | ||
| 455 | * | ||
| 456 | * @param cliOptions Cli options | ||
| 457 | * @param outputContainer Output container | ||
| 458 | * @return File extension without the leading dot | ||
| 459 | */ | ||
| 460 | 4322 | public f<string> getOutputFileExtension(const CliOptions& cliOptions, OutputContainer outputContainer) { | |
| 461 | // Extensions per output container: executable, object file, static library and shared library | ||
| 462 | 4322 | const string[4] extensionsWasm = ["wasm", "o", "a", "so"]; | |
| 463 | 4322 | const string[4] extensionsMacos = ["", "o", "a", "dylib"]; | |
| 464 | 4322 | const string[4] extensionsWindows = ["exe", "obj", "lib", "dll"]; | |
| 465 | 4322 | const string[4] extensionsLinux = ["", "o", "a", "so"]; | |
| 466 | |||
| 467 | 4322 | const String triple = String(cliOptions.targetTriple.value); | |
| 468 | 4322 | if triple.isEmpty() { | |
| 469 | − | if isWindows() { // LCOV_EXCL_LINE - coverage runs on Linux | |
| 470 | − | return extensionsWindows[outputContainer]; // LCOV_EXCL_LINE - coverage runs on Linux | |
| 471 | − | } else if isMacOS() { // LCOV_EXCL_LINE - coverage runs on Linux | |
| 472 | − | return extensionsMacos[outputContainer]; // LCOV_EXCL_LINE - coverage runs on Linux | |
| 473 | − | } // LCOV_EXCL_LINE - coverage runs on Linux | |
| 474 | 2144 | return extensionsLinux[outputContainer]; | |
| 475 | } | ||
| 476 | |||
| 477 | 3250 | if triple.startsWith("wasm") { | |
| 478 | 10 | return extensionsWasm[outputContainer]; | |
| 479 | } | ||
| 480 | 3245 | if isDarwinTriple(triple) { | |
| 481 | 66 | return extensionsMacos[outputContainer]; | |
| 482 | } | ||
| 483 | 3212 | if triple.contains("windows") || triple.contains("win32") || triple.contains("mingw") || triple.contains("cygwin") { | |
| 484 | 64 | return extensionsWindows[outputContainer]; | |
| 485 | } | ||
| 486 | 6360 | return extensionsLinux[outputContainer]; | |
| 487 | 4322 | } | |
| 488 | |||
| 489 | /** | ||
| 490 | * Retrieve the dir, where output binaries should go when installing them | ||
| 491 | * | ||
| 492 | * @return Installation directory | ||
| 493 | */ | ||
| 494 | 3 | public f<FilePath> getSpiceBinDir() { | |
| 495 | − | if isWindows() { // LCOV_EXCL_LINE - coverage runs on Linux | |
| 496 | − | Result<string> userprofile = getEnv("USERPROFILE"); // LCOV_EXCL_LINE - coverage runs on Linux | |
| 497 | − | return FilePath(userprofile.unwrap()) / "spice" / "bin"; // LCOV_EXCL_LINE - coverage runs on Linux | |
| 498 | 3 | } else if isUnixBased() { | |
| 499 | 3 | return FilePath("/usr/local/bin"); | |
| 500 | } else { // LCOV_EXCL_LINE | ||
| 501 | − | panic(Error("Unsupported platform")); // LCOV_EXCL_LINE | |
| 502 | − | } // LCOV_EXCL_LINE | |
| 503 | ✗ | } | |
| 504 | // ─── tests ──────────────────────────────────────────────────────────────────── | ||
| 505 | |||
| 506 | 1 | #[test] | |
| 507 | f<bool> testIsCommandAvailable() { | ||
| 508 | // Graphviz is optional, so the positive case is checked with a command, that always exists | ||
| 509 | 1 | assert isCommandAvailable(isWindows() ? "cmd" : "sh"); | |
| 510 | 1 | assert isGraphvizInstalled() == isCommandAvailable("dot"); | |
| 511 | 1 | assert !isCommandAvailable("non-existing-command"); | |
| 512 | 1 | return true; | |
| 513 | } | ||
| 514 | |||
| 515 | 1 | #[test] | |
| 516 | f<bool> testExpandLinkerFlag() { | ||
| 517 | // Plain flag | ||
| 518 | 1 | const Vector<String> plain = expandLinkerFlag(String("-lm")); | |
| 519 | 1 | assert plain.getSize() == 1l; | |
| 520 | 1 | assert plain.get(0l) == "-lm"; | |
| 521 | |||
| 522 | // Single env var | ||
| 523 | 1 | assert setEnv("SPICE_TEST_LINKER_FLAG_VAR", "/opt/llvm/lib"); | |
| 524 | 1 | const String singleFlag = String(isWindows() ? "-L%SPICE_TEST_LINKER_FLAG_VAR%" : "-L$SPICE_TEST_LINKER_FLAG_VAR"); | |
| 525 | 1 | const Vector<String> single = expandLinkerFlag(singleFlag); | |
| 526 | 1 | assert single.getSize() == 1l; | |
| 527 | 1 | assert single.get(0l) == "-L/opt/llvm/lib"; | |
| 528 | |||
| 529 | // An env var that expands to several whitespace-separated flags (e.g. LLVM_INCLUDE_DIRS, populated by CMake/CI with one | ||
| 530 | // '-I<dir>' per LLVM include directory) must be split into one argv entry per flag, since there is no shell here to | ||
| 531 | // word-split the expansion | ||
| 532 | 1 | assert setEnv("SPICE_TEST_LINKER_FLAG_MULTI", "-I/opt/llvm/include -I/opt/llvm/build/include"); | |
| 533 | 1 | const String multiFlag = String(isWindows() ? "%SPICE_TEST_LINKER_FLAG_MULTI%" : "$SPICE_TEST_LINKER_FLAG_MULTI"); | |
| 534 | 1 | const Vector<String> multi = expandLinkerFlag(multiFlag); | |
| 535 | 1 | assert multi.getSize() == 2l; | |
| 536 | 1 | assert multi.get(0l) == "-I/opt/llvm/include"; | |
| 537 | 1 | assert multi.get(1l) == "-I/opt/llvm/build/include"; | |
| 538 | 1 | return true; | |
| 539 | 1 | } | |
| 540 | |||
| 541 | 1 | #[test] | |
| 542 | f<bool> testGetLinkerFlagCommandArgs() { | ||
| 543 | 1 | Vector<String> pkgConfigTokens; | |
| 544 | 1 | pkgConfigTokens.pushBack(String("pkg-config")); | |
| 545 | 1 | pkgConfigTokens.pushBack(String("--cflags")); | |
| 546 | 1 | pkgConfigTokens.pushBack(String("--libs")); | |
| 547 | 1 | pkgConfigTokens.pushBack(String("openssl")); | |
| 548 | |||
| 549 | // A dynamic link passes the arguments through | ||
| 550 | 1 | const Vector<String> dynamicArgs = getLinkerFlagCommandArgs(pkgConfigTokens, false); | |
| 551 | 1 | assert dynamicArgs.getSize() == 3l; | |
| 552 | 1 | assert dynamicArgs.get(0l) == "--cflags"; | |
| 553 | 1 | assert dynamicArgs.get(1l) == "--libs"; | |
| 554 | 1 | assert dynamicArgs.get(2l) == "openssl"; | |
| 555 | |||
| 556 | // A static link asks pkg-config for the static flags, that include the private dependencies | ||
| 557 | 1 | const Vector<String> staticArgs = getLinkerFlagCommandArgs(pkgConfigTokens, true); | |
| 558 | 1 | assert staticArgs.getSize() == 4l; | |
| 559 | 1 | assert staticArgs.get(0l) == "--static"; | |
| 560 | 1 | assert staticArgs.get(1l) == "--cflags"; | |
| 561 | 1 | assert staticArgs.get(2l) == "--libs"; | |
| 562 | 1 | assert staticArgs.get(3l) == "openssl"; | |
| 563 | |||
| 564 | // Other commands are passed through for a static link as well | ||
| 565 | 1 | Vector<String> otherTokens; | |
| 566 | 1 | otherTokens.pushBack(String("llvm-config")); | |
| 567 | 1 | otherTokens.pushBack(String("--libs")); | |
| 568 | 1 | const Vector<String> otherArgs = getLinkerFlagCommandArgs(otherTokens, true); | |
| 569 | 1 | assert otherArgs.getSize() == 1l; | |
| 570 | 1 | assert otherArgs.get(0l) == "--libs"; | |
| 571 | 1 | return true; | |
| 572 | 1 | } | |
| 573 | |||
| 574 | 1 | #[test] | |
| 575 | f<bool> testGetStdTPDEFlags() { | ||
| 576 | // A std without the TPDE libraries and headers (e.g. a source checkout) yields no flags | ||
| 577 | 1 | const FilePath stdDir = makeUniqueTestDir("spice-test-std-tpde"); | |
| 578 | 1 | const FilePath tpdeDir = stdDir / "bindings" / "tpde"; | |
| 579 | 1 | const FilePath includeDir = tpdeDir / "include"; | |
| 580 | 1 | const FilePath headerDir = includeDir / "tpde-llvm"; | |
| 581 | 1 | const FilePath libDir = tpdeDir / "lib"; | |
| 582 | 1 | createTestDirs(headerDir); | |
| 583 | 1 | createTestDirs(libDir); | |
| 584 | 1 | const String flagsWithoutTpde = getStdTPDEFlags(stdDir); | |
| 585 | |||
| 586 | // A std with the TPDE libraries and headers of a release package yields the include flag and the libraries, that exist, | ||
| 587 | // in link order and inside a linker group | ||
| 588 | 1 | const FilePath tpdeLlvmLibPath = libDir / "libtpde_llvm.a"; | |
| 589 | 1 | const FilePath tpdeLibPath = libDir / "libtpde.a"; | |
| 590 | 1 | const FilePath fadecLibPath = libDir / "libfadec.a"; | |
| 591 | 1 | writeTestFile(headerDir / "LLVMCompiler.hpp", "\n"); | |
| 592 | 1 | writeTestFile(tpdeLlvmLibPath, "\n"); | |
| 593 | 1 | writeTestFile(tpdeLibPath, "\n"); | |
| 594 | 1 | writeTestFile(fadecLibPath, "\n"); | |
| 595 | 1 | const String expected = String("-I") + includeDir.toString() + " -Wl,--start-group " + tpdeLlvmLibPath.toString() + " " + tpdeLibPath.toString() + " " + fadecLibPath.toString() + " -Wl,--end-group"; | |
| 596 | 1 | const String flags = getStdTPDEFlags(stdDir); | |
| 597 | |||
| 598 | // Without the TPDE LLVM library, the bindings can't use TPDE at all | ||
| 599 | 1 | assert deleteFile(tpdeLlvmLibPath.toString()); | |
| 600 | 1 | const String flagsWithoutTpdeLlvm = getStdTPDEFlags(stdDir); | |
| 601 | |||
| 602 | // Clean up before asserting, so that a failure does not leave the files behind | ||
| 603 | 1 | removeTestDir(stdDir); | |
| 604 | 1 | assert flagsWithoutTpde.isEmpty(); | |
| 605 | 1 | assert flags == expected; | |
| 606 | 1 | assert flagsWithoutTpdeLlvm.isEmpty(); | |
| 607 | 1 | return true; | |
| 608 | 1 | } | |
| 609 |