GCC Code Coverage Report


src/
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
244 / 310, 37 excl
78.7%
Branches:
107 / 174, 82 excl
61.5%
Functions:
26 / 29
89.7%

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
1/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✓ Branch 2 taken 467 times.
✗ Branch 3 not taken.
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
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ 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
0/2
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ 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
4/4
✓ Branch 0 taken 7 times.
✓ Branch 1 taken 3 times.
✓ Branch 2 taken 19422 times.
✓ Branch 3 taken 1854 times.
21286 while i < flag.getLength() {
117 19429 const char c = flag[i];
118
4/8
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 7 times.
✓ Branch 4 taken 19422 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 19422 times.
19429 if isWindows() && c == '%' {
119 ✗ const long end = flag.find('%', i + 1l);
120
0/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ 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
0/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ if value.isOk() {
124 ✗ expanded += value.unwrap();
125 ✗ }
126 ✗ i = cast<unsigned long>(end) + 1l;
127 ✗ continue;
128 ✗ }
129
6/8
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 19422 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 19415 times.
✓ Branch 7 taken 7 times.
19429 } else if !isWindows() && c == '$' {
130 9 unsigned long j = i + 1l;
131
8/8
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 54 times.
✓ Branch 2 taken 54 times.
✓ Branch 3 taken 2 times.
✓ Branch 4 taken 7 times.
✓ Branch 5 taken 97 times.
✓ Branch 6 taken 97 times.
✓ Branch 7 taken 7 times.
160 while j < flag.getLength() && isAlphaNumUnderscore(flag[j]) {
132 151 j++;
133 151 }
134
2/4
✓ Branch 0 taken 2 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✗ Branch 3 not taken.
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
2/4
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
✗ Branch 2 not taken.
✓ Branch 3 taken 7 times.
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
6/12
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 3 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 3 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1854 times.
✗ Branch 7 not taken.
✓ Branch 8 taken 1854 times.
✗ Branch 9 not taken.
✓ Branch 10 taken 1854 times.
✗ Branch 11 not taken.
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
0/4
✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ 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
4/4
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 2 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 1 time.
3 if staticLinking && tokens.front() == "pkg-config" {
184 1 cmdArgs.pushBack(String("--static"));
185 1 }
186
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 7 times.
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
2/2
✓ Branch 0 taken 1857 times.
✓ Branch 1 taken 20323 times.
22180 for unsigned long i = 0l; i < input.getLength(); i++ {
202 20323 const char c = input[i];
203
8/12
✓ Branch 0 taken 20312 times.
✓ Branch 1 taken 11 times.
✓ Branch 2 taken 20312 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 20312 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 20312 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 20312 times.
✓ Branch 10 taken 11 times.
✓ Branch 11 taken 20312 times.
20323 if c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\v' || c == '\f' {
204
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 11 times.
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
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1857 times.
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
1/2
✓ Branch 0 taken 932 times.
✗ Branch 1 not taken.
932 foreach const string linkerInvoker : ["clang", "gcc"] {
247
1/2
✓ Branch 0 taken 932 times.
✗ Branch 1 not taken.
932 if isUnixBased() {
248
1/2
✓ Branch 0 taken 932 times.
✗ Branch 1 not taken.
932 foreach const string path : ["/usr/bin/", "/usr/local/bin/", "/bin/"] {
249 932 const String linkerInvokerPath = path + linkerInvoker;
250
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 932 times.
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
1/2
✓ Branch 0 taken 471 times.
✗ Branch 1 not taken.
471 if isUnixBased() {
275 // Use the flavor-specific lld driver, since the generic 'lld' driver refuses to be invoked directly
276
1/2
✓ Branch 0 taken 471 times.
✗ Branch 1 not taken.
471 foreach const string linkerName : ["mold", "ld.lld", "ld64.lld", "gold", "ld"] {
277 // mold does only support linking for unix and darwin
278
2/4
✓ Branch 0 taken 471 times.
✗ Branch 1 not taken.
✗ Branch 2 not taken.
✓ Branch 3 taken 471 times.
471 if linkerName == "mold" && cliOptions.targetTriple.isOSWindows() {
279 ✗ continue;
280 }
281
1/2
✓ Branch 0 taken 942 times.
✗ Branch 1 not taken.
942 foreach const string path : ["/usr/bin/", "/usr/local/bin/", "/bin/"] {
282 942 String linkerPath = path + linkerName;
283
2/2
✓ Branch 0 taken 471 times.
✓ Branch 1 taken 471 times.
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/2
✓ Branch 0 taken 1 time.
✗ Branch 1 not taken.
1 if isUnixBased() {
310
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 2 times.
2 foreach const string archiverName : ["llvm-ar", "gcc-ar", "ar"] {
311
2/2
✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1 time.
5 foreach const string path : ["/usr/bin/", "/usr/local/bin/", "/bin/"] {
312 4 String archiverPath = path + archiverName;
313
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 time.
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
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5379 times.
5379 if isUnixBased() {
340 5379 const FilePath installedStdPath = FilePath("/usr/lib/spice/std/");
341
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5379 times.
5379 if installedStdPath.exists() { return installedStdPath; }
342 5379 }
343 5379 Result<string> value = getEnv("SPICE_STD_DIR");
344
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 5379 times.
5379 if value.isOk() {
345 5379 const FilePath envStdPath = FilePath(value.unwrap());
346
1/2
✓ Branch 0 taken 5379 times.
✗ Branch 1 not taken.
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
1/2
✓ Branch 0 taken 3873 times.
✗ Branch 1 not taken.
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
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 471 times.
471 if stdDir.isEmpty() {
373 ✗ return FilePath();
374 }
375 471 result = stdDir / "runtime" / "lib";
376
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 471 times.
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
4/4
✓ Branch 0 taken 1 time.
✓ Branch 1 taken 1037 times.
✓ Branch 2 taken 2 times.
✓ Branch 3 taken 1036 times.
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
2/2
✓ Branch 0 taken 5180 times.
✓ Branch 1 taken 1036 times.
6216 foreach const string libName : ["libtpde_llvm.a", "libtpde.a", "libfadec.a", "libdisarm64.a", "libspdlog.a"] {
400 5180 FilePath libPath = libDir / libName;
401
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 5178 times.
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
3/4
✗ Branch 0 not taken.
✓ Branch 1 taken 1052 times.
✓ Branch 2 taken 17 times.
✓ Branch 3 taken 1035 times.
1052 if !isLinux() || !cliOptions.isNativeTarget {
418 17 return;
419 }
420 1035 const Result<string> existingFlags = getEnv("TPDE_FLAGS");
421
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1035 times.
1035 if existingFlags.isOk() {
422 ✗ return;
423 }
424 1035 const FilePath stdDir = findStdDir();
425
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1035 times.
1035 if stdDir.isEmpty() {
426 ✗ return;
427 }
428 1035 const String flags = getStdTPDEFlags(stdDir);
429
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1035 times.
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
2/2
✓ Branch 0 taken 28988 times.
✓ Branch 1 taken 3212 times.
32200 for unsigned int i = 0; i < len(darwinOsNames); i++ {
445
2/2
✓ Branch 0 taken 33 times.
✓ Branch 1 taken 28955 times.
28988 if triple.contains(darwinOsNames[i]) {
446 33 return true;
447 }
448 28955 }
449
1/2
✓ Branch 0 taken 3212 times.
✗ Branch 1 not taken.
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
2/2
✓ Branch 0 taken 1072 times.
✓ Branch 1 taken 3250 times.
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
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 3245 times.
3250 if triple.startsWith("wasm") {
478 10 return extensionsWasm[outputContainer];
479 }
480
2/2
✓ Branch 0 taken 33 times.
✓ Branch 1 taken 3212 times.
3245 if isDarwinTriple(triple) {
481 66 return extensionsMacos[outputContainer];
482 }
483
6/8
✓ Branch 0 taken 3180 times.
✓ Branch 1 taken 32 times.
✓ Branch 2 taken 3180 times.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 3180 times.
✓ Branch 6 taken 32 times.
✓ Branch 7 taken 3180 times.
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
1/2
✓ Branch 0 taken 3 times.
✗ Branch 1 not taken.
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