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
;
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
;
41 * Utilities for CMake.
45 public final class CMakeUtils
57 * Returns the path where CMake is available.
59 * @return The CMake path.
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
))
86 if (cmakePath
== null &&
87 OperatingSystem
.current() == OperatingSystem
.MAC_OS
)
89 Path maybe
= Paths
.get("/").resolve("opt")
90 .resolve("homebrew").resolve("bin")
92 if (Files
.exists(maybe
))
100 * Requests the version of CMake.
102 * @return The resultant CMake version or {@code null} if there is no
106 public static VersionNumber
cmakeExeVersion()
108 // We need the CMake executable
109 Path cmakeExe
= CMakeUtils
.cmakeExePath();
110 if (cmakeExe
== null)
115 String rawStr
= CMakeUtils
.cmakeExecuteOutput("version",
118 // Read in what looks like a version number
119 try (BufferedReader buf
= new BufferedReader(
120 new StringReader(rawStr
)))
124 String ln
= buf
.readLine();
128 // Remove any whitespace and make it lowercase so it is
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());
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.",
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.
161 public static int cmakeExecute(Logger __logger
, String __logName
,
162 Path __logDir
, String
... __args
)
165 if (__logDir
== null)
166 throw new NullPointerException("NARG");
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
))
185 return CMakeUtils
.cmakeExecutePipe(null, stdOut
, stdErr
,
189 // Dump logs to Gradle
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.
207 public static String
cmakeExecuteOutput(String __buildType
,
211 try (ByteArrayOutputStream baos
= new ByteArrayOutputStream())
214 CMakeUtils
.cmakeExecutePipe(null, baos
, null,
215 __buildType
, __args
);
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.
234 public static int cmakeExecutePipe(InputStream __in
,
235 OutputStream __out
, OutputStream __err
, String __buildType
,
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.
256 public static int cmakeExecutePipe(boolean __fail
, InputStream __in
,
257 OutputStream __out
, OutputStream __err
, String __buildType
,
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
278 procBuilder
.redirectInput(ProcessBuilder
.Redirect
.PIPE
);
280 procBuilder
.redirectOutput(ProcessBuilder
.Redirect
.PIPE
);
282 procBuilder
.redirectError(ProcessBuilder
.Redirect
.PIPE
);
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
)))
295 fwdOut
.runThread("cmake-stdout");
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
);
323 * Configures the CMake task.
325 * @param __task The task to configure.
326 * @throws IOException If it could not be configured.
329 public static void configure(CMakeBuildTask __task
)
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.
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")))
363 // Load in the CMake cache to check it
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)
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
))
385 catch (IOException __ignored
)
387 // If this happens, just assume it needs to be done
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.
403 public static void dumpLog(Logger __logger
, LogLevel __level
, Path __pathy
)
407 for (String ln
: Files
.readAllLines(__pathy
))
408 __logger
.log(__level
, ln
);
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.
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")))
437 if (line
.startsWith("//") || line
.startsWith("#"))
440 // Find equal sign between key and value
441 int eq
= line
.indexOf('=');
446 result
.put(line
.substring(0, eq
).trim(),
447 line
.substring(eq
+ 1).trim());
450 // Return parsed properties