Detect whether the CMake build tree changes locations, such as when CI/CD uses a...
[SquirrelJME.git] / buildSrc / src / main / java / cc / squirreljme / plugin / general / cmake / CMakeUtils.java
blob42ba2c6891f6b5b9cbe565e9fec1c6cf38ad2759
1 // -*- Mode: Java; indent-tabs-mode: t; tab-width: 4 -*-
2 // ---------------------------------------------------------------------------
3 // Multi-Phasic Applications: SquirrelJME
4 // Copyright (C) Stephanie Gawroriski <xer@multiphasicapps.net>
5 // ---------------------------------------------------------------------------
6 // SquirrelJME is under the Mozilla Public License Version 2.0.
7 // See license.mkd for licensing and copyright information.
8 // ---------------------------------------------------------------------------
10 package cc.squirreljme.plugin.general.cmake;
12 import cc.squirreljme.plugin.multivm.VMHelpers;
13 import cc.squirreljme.plugin.util.ForwardInputToOutput;
14 import cc.squirreljme.plugin.util.ForwardStream;
15 import cc.squirreljme.plugin.util.PathUtils;
16 import java.io.BufferedReader;
17 import java.io.ByteArrayOutputStream;
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.io.OutputStream;
21 import java.io.Reader;
22 import java.io.StringReader;
23 import java.nio.file.Files;
24 import java.nio.file.Path;
25 import java.nio.file.Paths;
26 import java.nio.file.StandardOpenOption;
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.LinkedHashMap;
30 import java.util.List;
31 import java.util.Locale;
32 import java.util.Map;
33 import java.util.concurrent.TimeUnit;
34 import org.gradle.api.Task;
35 import org.gradle.api.logging.LogLevel;
36 import org.gradle.api.logging.Logger;
37 import org.gradle.internal.os.OperatingSystem;
38 import org.gradle.util.internal.VersionNumber;
40 /**
41 * Utilities for CMake.
43 * @since 2024/03/15
45 public final class CMakeUtils
47 /**
48 * Not used.
50 * @since 2024/03/15
52 private CMakeUtils()
56 /**
57 * Returns the path where CMake is available.
59 * @return The CMake path.
60 * @since 2024/03/15
62 public static Path cmakeExePath()
64 // Standard executable?
65 Path cmakePath = PathUtils.findPath("cmake");
67 // Windows executable?
68 if (cmakePath == null)
69 cmakePath = PathUtils.findPath("cmake.exe");
71 // Standard installation on Windows?
72 if (cmakePath == null &&
73 OperatingSystem.current() == OperatingSystem.WINDOWS)
75 String programFiles = System.getenv("PROGRAMFILES");
76 if (programFiles != null)
78 Path maybe = Paths.get(programFiles).resolve("CMake")
79 .resolve("bin").resolve("cmake.exe");
80 if (Files.exists(maybe))
81 cmakePath = maybe;
85 // Homebrew on macOS?
86 if (cmakePath == null &&
87 OperatingSystem.current() == OperatingSystem.MAC_OS)
89 Path maybe = Paths.get("/").resolve("opt")
90 .resolve("homebrew").resolve("bin")
91 .resolve("cmake");
92 if (Files.exists(maybe))
93 cmakePath = maybe;
96 return cmakePath;
99 /**
100 * Requests the version of CMake.
102 * @return The resultant CMake version or {@code null} if there is no
103 * CMake available.
104 * @since 2024/04/01
106 public static VersionNumber cmakeExeVersion()
108 // We need the CMake executable
109 Path cmakeExe = CMakeUtils.cmakeExePath();
110 if (cmakeExe == null)
111 return null;
115 String rawStr = CMakeUtils.cmakeExecuteOutput("version",
116 "--version");
118 // Read in what looks like a version number
119 try (BufferedReader buf = new BufferedReader(
120 new StringReader(rawStr)))
122 for (;;)
124 String ln = buf.readLine();
125 if (ln == null)
126 break;
128 // Remove any whitespace and make it lowercase so it is
129 // easier to parse
130 ln = ln.trim().toLowerCase(Locale.ROOT);
132 // Is this the version string?
133 if (ln.startsWith("cmake version"))
134 return VersionNumber.parse(
135 ln.substring("cmake version".length()).trim());
139 // Failed
140 throw new RuntimeException(
141 "CMake executed but there was no version.");
143 catch (IOException __e)
145 throw new RuntimeException("Could not determine CMake version.",
146 __e);
151 * Executes a CMake task.
153 * @param __logger The logger to use.
154 * @param __logName The name of the log.
155 * @param __logDir The CMake build directory.
156 * @param __args CMake arguments.
157 * @return The CMake exit value.
158 * @throws IOException On read/write or execution errors.
159 * @since 2024/03/15
161 public static int cmakeExecute(Logger __logger, String __logName,
162 Path __logDir, String... __args)
163 throws IOException
165 if (__logDir == null)
166 throw new NullPointerException("NARG");
168 // Output log files
169 Path outLog = __logDir.resolve(__logName + ".out");
170 Path errLog = __logDir.resolve(__logName + ".err");
172 // Make sure directories exist first
173 Files.createDirectories(outLog.getParent());
174 Files.createDirectories(errLog.getParent());
176 // Run with log wrapping
177 try (OutputStream stdOut = Files.newOutputStream(outLog,
178 StandardOpenOption.CREATE, StandardOpenOption.WRITE,
179 StandardOpenOption.TRUNCATE_EXISTING);
180 OutputStream stdErr = Files.newOutputStream(errLog,
181 StandardOpenOption.CREATE, StandardOpenOption.WRITE,
182 StandardOpenOption.TRUNCATE_EXISTING))
184 // Execute CMake
185 return CMakeUtils.cmakeExecutePipe(null, stdOut, stdErr,
186 __logName, __args);
189 // Dump logs to Gradle
190 finally
192 CMakeUtils.dumpLog(__logger,
193 LogLevel.LIFECYCLE, outLog);
194 CMakeUtils.dumpLog(__logger,
195 LogLevel.ERROR, errLog);
200 * Executes a CMake task and returns the output as a string.
202 * @param __buildType The build type used.
203 * @param __args CMake arguments.
204 * @throws IOException On read/write or execution errors.
205 * @since 2024/04/01
207 public static String cmakeExecuteOutput(String __buildType,
208 String... __args)
209 throws IOException
211 try (ByteArrayOutputStream baos = new ByteArrayOutputStream())
213 // Run command
214 CMakeUtils.cmakeExecutePipe(null, baos, null,
215 __buildType, __args);
217 // Decode to output
218 return baos.toString("utf-8");
223 * Executes a CMake task to the given pipe.
225 * @param __in The standard input for the task.
226 * @param __out The standard output for the task.
227 * @param __err The standard error for the task.
228 * @param __buildType The build type used.
229 * @param __args CMake arguments.
230 * @return The CMake exit value.
231 * @throws IOException On read/write or execution errors.
232 * @since 2024/04/01
234 public static int cmakeExecutePipe(InputStream __in,
235 OutputStream __out, OutputStream __err, String __buildType,
236 String... __args)
237 throws IOException
239 return CMakeUtils.cmakeExecutePipe(true, __in, __out, __err,
240 __buildType, __args);
244 * Executes a CMake task to the given pipe.
246 * @param __fail Emit failure if exit status is non-zero.
247 * @param __in The standard input for the task.
248 * @param __out The standard output for the task.
249 * @param __err The standard error for the task.
250 * @param __buildType The build type used.
251 * @param __args CMake arguments.
252 * @return The CMake exit value.
253 * @throws IOException On read/write or execution errors.
254 * @since 2024/04/01
256 public static int cmakeExecutePipe(boolean __fail, InputStream __in,
257 OutputStream __out, OutputStream __err, String __buildType,
258 String... __args)
259 throws IOException
261 // Need CMake
262 Path cmakePath = CMakeUtils.cmakeExePath();
263 if (cmakePath == null)
264 throw new RuntimeException("CMake not found.");
266 // Determine run arguments
267 List<String> args = new ArrayList<>();
268 args.add(cmakePath.toAbsolutePath().toString());
269 if (__args != null && __args.length > 0)
270 args.addAll(Arrays.asList(__args));
272 // Set executable process
273 ProcessBuilder procBuilder = new ProcessBuilder();
274 procBuilder.command(args);
276 // Log the output somewhere
277 if (__in != null)
278 procBuilder.redirectInput(ProcessBuilder.Redirect.PIPE);
279 if (__out != null)
280 procBuilder.redirectOutput(ProcessBuilder.Redirect.PIPE);
281 if (__err != null)
282 procBuilder.redirectError(ProcessBuilder.Redirect.PIPE);
284 // Start the process
285 Process proc = procBuilder.start();
287 // Wait for it to complete
288 try (ForwardStream fwdOut = (__out == null ? null :
289 new ForwardInputToOutput(proc.getInputStream(), __out));
290 ForwardStream fwdErr = (__err == null ? null :
291 new ForwardInputToOutput(proc.getErrorStream(), __err)))
293 // Forward output
294 if (__out != null)
295 fwdOut.runThread("cmake-stdout");
297 // Forward error
298 if (__err != null)
299 fwdErr.runThread("cmake-stderr");
301 // Wait for completion, stop if it takes too long
302 if (!proc.waitFor(15, TimeUnit.MINUTES) ||
303 (__fail && proc.exitValue() != 0))
304 throw new RuntimeException(String.format(
305 "CMake failed to %s: exit value %d",
306 __buildType, proc.exitValue()));
308 // Use the given exit value
309 return proc.exitValue();
311 catch (InterruptedException|IllegalThreadStateException __e)
313 throw new RuntimeException("CMake timed out or stuck!", __e);
315 finally
317 // Destroy the task
318 proc.destroy();
323 * Configures the CMake task.
325 * @param __task The task to configure.
326 * @throws IOException If it could not be configured.
327 * @since 2024/04/08
329 public static void configure(CMakeBuildTask __task)
330 throws IOException
332 Path cmakeBuild = __task.cmakeBuild;
333 Path cmakeSource = __task.cmakeSource;
335 // Old directory must be deleted as it might be very stale
336 VMHelpers.deleteDirTree(__task, cmakeBuild);
338 // Make sure the output build directory exists
339 Files.createDirectories(cmakeBuild);
341 // Configure CMake first before we continue with anything
342 CMakeUtils.cmakeExecute(__task.getLogger(),
343 "configure", __task.getProject().getBuildDir().toPath(),
344 "-S", cmakeSource.toAbsolutePath().toString(),
345 "-B", cmakeBuild.toAbsolutePath().toString());
348 * Is configuration needed?
350 * @param __task The task to check.
351 * @return If reconfiguration is needed or not.
352 * @since 2024/04/08
354 public static boolean configureNeeded(CMakeBuildTask __task)
356 Path cmakeBuild = __task.cmakeBuild;
358 // Missing directories or no cache at all?
359 if (!Files.isDirectory(cmakeBuild) ||
360 !Files.exists(cmakeBuild.resolve("CMakeCache.txt")))
361 return true;
363 // Load in the CMake cache to check it
366 // Load CMake cache
367 Map<String, String> cmakeCache = CMakeUtils.loadCache(cmakeBuild);
369 // Check the configuration directory
370 String rawConfigDir = cmakeCache.get(
371 "CMAKE_CACHEFILE_DIR:INTERNAL");
373 // No configuration directory is known??
374 if (rawConfigDir == null)
375 return true;
377 // Did the directory of the cache change? This can happen
378 // under CI/CD where the build directory is different and
379 // there is old data that is restored
380 Path configDir = Paths.get(rawConfigDir).toAbsolutePath();
381 if (!Files.isSameFile(configDir, cmakeBuild) ||
382 !cmakeBuild.equals(configDir))
383 return true;
385 catch (IOException __ignored)
387 // If this happens, just assume it needs to be done
388 return true;
391 // Not needed
392 return false;
396 * Dumps log to the output.
398 * @param __logger The logger to output to.
399 * @param __level The log level.
400 * @param __pathy The output path.
401 * @since 2024/03/15
403 public static void dumpLog(Logger __logger, LogLevel __level, Path __pathy)
407 for (String ln : Files.readAllLines(__pathy))
408 __logger.log(__level, ln);
410 catch (Throwable e)
412 e.printStackTrace();
417 * Loads the CMake cache from the given build.
419 * @param __logDir The build directory to use.
420 * @return The CMake cache.
421 * @throws IOException If it could not be read.
422 * @throws NullPointerException On null arguments.
423 * @since 2024/04/01
425 public static Map<String, String> loadCache(Path __logDir)
426 throws IOException, NullPointerException
428 if (__logDir == null)
429 throw new NullPointerException("NARG");
431 // Load in lines accordingly
432 Map<String, String> result = new LinkedHashMap<>();
433 for (String line : Files.readAllLines(
434 __logDir.resolve("CMakeCache.txt")))
436 // Comment?
437 if (line.startsWith("//") || line.startsWith("#"))
438 continue;
440 // Find equal sign between key and value
441 int eq = line.indexOf('=');
442 if (eq < 0)
443 continue;
445 // Split in
446 result.put(line.substring(0, eq).trim(),
447 line.substring(eq + 1).trim());
450 // Return parsed properties
451 return result;