Ensure log directory exists first.
[SquirrelJME.git] / buildSrc / src / main / java / cc / squirreljme / plugin / general / cmake / CMakeUtils.java
blob303ef262effce707e06814536ccd82e64412daf3
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.util.ForwardInputToOutput;
13 import cc.squirreljme.plugin.util.ForwardStream;
14 import cc.squirreljme.plugin.util.PathUtils;
15 import java.io.BufferedReader;
16 import java.io.ByteArrayOutputStream;
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.io.OutputStream;
20 import java.io.Reader;
21 import java.io.StringReader;
22 import java.nio.file.Files;
23 import java.nio.file.Path;
24 import java.nio.file.Paths;
25 import java.nio.file.StandardOpenOption;
26 import java.util.ArrayList;
27 import java.util.Arrays;
28 import java.util.LinkedHashMap;
29 import java.util.List;
30 import java.util.Locale;
31 import java.util.Map;
32 import java.util.concurrent.TimeUnit;
33 import org.gradle.api.logging.LogLevel;
34 import org.gradle.api.logging.Logger;
35 import org.gradle.internal.os.OperatingSystem;
36 import org.gradle.util.internal.VersionNumber;
38 /**
39 * Utilities for CMake.
41 * @since 2024/03/15
43 public final class CMakeUtils
45 /**
46 * Not used.
48 * @since 2024/03/15
50 private CMakeUtils()
54 /**
55 * Returns the path where CMake is available.
57 * @return The CMake path.
58 * @since 2024/03/15
60 public static Path cmakeExePath()
62 // Standard executable?
63 Path cmakePath = PathUtils.findPath("cmake");
65 // Windows executable?
66 if (cmakePath == null)
67 cmakePath = PathUtils.findPath("cmake.exe");
69 // Standard installation on Windows?
70 if (OperatingSystem.current() == OperatingSystem.WINDOWS)
72 String programFiles = System.getenv("PROGRAMFILES");
73 if (programFiles != null)
75 Path maybe = Paths.get(programFiles).resolve("CMake")
76 .resolve("bin").resolve("cmake.exe");
77 if (Files.exists(maybe))
78 return maybe;
82 return cmakePath;
85 /**
86 * Requests the version of CMake.
88 * @return The resultant CMake version or {@code null} if there is no
89 * CMake available.
90 * @since 2024/04/01
92 public static VersionNumber cmakeExeVersion()
94 // We need the CMake executable
95 Path cmakeExe = CMakeUtils.cmakeExePath();
96 if (cmakeExe == null)
97 return null;
99 try
101 String rawStr = CMakeUtils.cmakeExecuteOutput("version",
102 "--version");
104 // Read in what looks like a version number
105 try (BufferedReader buf = new BufferedReader(
106 new StringReader(rawStr)))
108 for (;;)
110 String ln = buf.readLine();
111 if (ln == null)
112 break;
114 // Remove any whitespace and make it lowercase so it is
115 // easier to parse
116 ln = ln.trim().toLowerCase(Locale.ROOT);
118 // Is this the version string?
119 if (ln.startsWith("cmake version"))
120 return VersionNumber.parse(
121 ln.substring("cmake version".length()).trim());
125 // Failed
126 throw new RuntimeException(
127 "CMake executed but there was no version.");
129 catch (IOException __e)
131 throw new RuntimeException("Could not determine CMake version.",
132 __e);
137 * Executes a CMake task.
139 * @param __logger The logger to use.
140 * @param __logName The name of the log.
141 * @param __cmakeBuild The CMake build directory.
142 * @param __args CMake arguments.
143 * @return The CMake exit value.
144 * @throws IOException On read/write or execution errors.
145 * @since 2024/03/15
147 public static int cmakeExecute(Logger __logger, String __logName,
148 Path __cmakeBuild, String... __args)
149 throws IOException
151 if (__cmakeBuild == null)
152 throw new NullPointerException("NARG");
154 // Output log files
155 Path outLog = __cmakeBuild.resolve(__logName + ".out");
156 Path errLog = __cmakeBuild.resolve(__logName + ".err");
158 // Make sure directories exist first
159 Files.createDirectories(outLog.getParent());
160 Files.createDirectories(errLog.getParent());
162 // Run with log wrapping
163 try (OutputStream stdOut = Files.newOutputStream(outLog,
164 StandardOpenOption.CREATE, StandardOpenOption.WRITE,
165 StandardOpenOption.TRUNCATE_EXISTING);
166 OutputStream stdErr = Files.newOutputStream(errLog,
167 StandardOpenOption.CREATE, StandardOpenOption.WRITE,
168 StandardOpenOption.TRUNCATE_EXISTING))
170 // Execute CMake
171 return CMakeUtils.cmakeExecutePipe(null, stdOut, stdErr,
172 __logName, __args);
175 // Dump logs to Gradle
176 finally
178 CMakeUtils.dumpLog(__logger,
179 LogLevel.LIFECYCLE, outLog);
180 CMakeUtils.dumpLog(__logger,
181 LogLevel.ERROR, errLog);
186 * Executes a CMake task and returns the output as a string.
188 * @param __buildType The build type used.
189 * @param __args CMake arguments.
190 * @throws IOException On read/write or execution errors.
191 * @since 2024/04/01
193 public static String cmakeExecuteOutput(String __buildType,
194 String... __args)
195 throws IOException
197 try (ByteArrayOutputStream baos = new ByteArrayOutputStream())
199 // Run command
200 CMakeUtils.cmakeExecutePipe(null, baos, null,
201 __buildType, __args);
203 // Decode to output
204 return baos.toString("utf-8");
209 * Executes a CMake task to the given pipe.
211 * @param __in The standard input for the task.
212 * @param __out The standard output for the task.
213 * @param __err The standard error for the task.
214 * @param __buildType The build type used.
215 * @param __args CMake arguments.
216 * @return The CMake exit value.
217 * @throws IOException On read/write or execution errors.
218 * @since 2024/04/01
220 public static int cmakeExecutePipe(InputStream __in,
221 OutputStream __out, OutputStream __err, String __buildType,
222 String... __args)
223 throws IOException
225 return CMakeUtils.cmakeExecutePipe(true, __in, __out, __err,
226 __buildType, __args);
230 * Executes a CMake task to the given pipe.
232 * @param __fail Emit failure if exit status is non-zero.
233 * @param __in The standard input for the task.
234 * @param __out The standard output for the task.
235 * @param __err The standard error for the task.
236 * @param __buildType The build type used.
237 * @param __args CMake arguments.
238 * @return The CMake exit value.
239 * @throws IOException On read/write or execution errors.
240 * @since 2024/04/01
242 public static int cmakeExecutePipe(boolean __fail, InputStream __in,
243 OutputStream __out, OutputStream __err, String __buildType,
244 String... __args)
245 throws IOException
247 // Need CMake
248 Path cmakePath = CMakeUtils.cmakeExePath();
249 if (cmakePath == null)
250 throw new RuntimeException("CMake not found.");
252 // Determine run arguments
253 List<String> args = new ArrayList<>();
254 args.add(cmakePath.toAbsolutePath().toString());
255 if (__args != null && __args.length > 0)
256 args.addAll(Arrays.asList(__args));
258 // Set executable process
259 ProcessBuilder procBuilder = new ProcessBuilder();
260 procBuilder.command(args);
262 // Log the output somewhere
263 if (__in != null)
264 procBuilder.redirectInput(ProcessBuilder.Redirect.PIPE);
265 if (__out != null)
266 procBuilder.redirectOutput(ProcessBuilder.Redirect.PIPE);
267 if (__err != null)
268 procBuilder.redirectError(ProcessBuilder.Redirect.PIPE);
270 // Start the process
271 Process proc = procBuilder.start();
273 // Wait for it to complete
274 try (ForwardStream fwdOut = (__out == null ? null :
275 new ForwardInputToOutput(proc.getInputStream(), __out));
276 ForwardStream fwdErr = (__err == null ? null :
277 new ForwardInputToOutput(proc.getErrorStream(), __err)))
279 // Forward output
280 if (__out != null)
281 fwdOut.runThread("cmake-stdout");
283 // Forward error
284 if (__err != null)
285 fwdErr.runThread("cmake-stderr");
287 // Wait for completion, stop if it takes too long
288 if (!proc.waitFor(15, TimeUnit.MINUTES) ||
289 (__fail && proc.exitValue() != 0))
290 throw new RuntimeException(String.format(
291 "CMake failed to %s...", __buildType));
293 // Use the given exit value
294 return proc.exitValue();
296 catch (InterruptedException __e)
298 throw new RuntimeException("CMake timed out!", __e);
300 finally
302 // Destroy the task
303 proc.destroy();
308 * Dumps log to the output.
310 * @param __logger The logger to output to.
311 * @param __level The log level.
312 * @param __pathy The output path.
313 * @since 2024/03/15
315 public static void dumpLog(Logger __logger, LogLevel __level, Path __pathy)
319 for (String ln : Files.readAllLines(__pathy))
320 __logger.log(__level, ln);
322 catch (Throwable e)
324 e.printStackTrace();
329 * Loads the CMake cache from the given build.
331 * @param __cmakeBuild The build directory to use.
332 * @return The CMake cache.
333 * @throws IOException If it could not be read.
334 * @throws NullPointerException On null arguments.
335 * @since 2024/04/01
337 public static Map<String, String> loadCache(Path __cmakeBuild)
338 throws IOException, NullPointerException
340 if (__cmakeBuild == null)
341 throw new NullPointerException("NARG");
343 // Load in lines accordingly
344 Map<String, String> result = new LinkedHashMap<>();
345 for (String line : Files.readAllLines(
346 __cmakeBuild.resolve("CMakeCache.txt")))
348 // Comment?
349 if (line.startsWith("//") || line.startsWith("#"))
350 continue;
352 // Find equal sign between key and value
353 int eq = line.indexOf('=');
354 if (eq < 0)
355 continue;
357 // Split in
358 result.put(line.substring(0, eq).trim(),
359 line.substring(eq + 1).trim());
362 // Return parsed properties
363 return result;