Commonize into PathUtils; On Linux/BSD try to use xdg-open/x-www-browser if Java...
[SquirrelJME.git] / buildSrc / src / main / java / cc / squirreljme / plugin / util / FossilExe.java
blobaa58e54ef6ff94663b1d2ec1bf6ca2470fce7ea0
1 // -*- Mode: Java; indent-tabs-mode: t; tab-width: 4 -*-
2 // ---------------------------------------------------------------------------
3 // SquirrelJME
4 // Copyright (C) Stephanie Gawroriski <xer@multiphasicapps.net>
5 // ---------------------------------------------------------------------------
6 // SquirrelJME is under the GNU General Public License v3+, or later.
7 // See license.mkd for licensing and copyright information.
8 // ---------------------------------------------------------------------------
10 package cc.squirreljme.plugin.util;
12 import java.io.BufferedReader;
13 import java.io.ByteArrayInputStream;
14 import java.io.ByteArrayOutputStream;
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.io.InputStreamReader;
18 import java.nio.charset.StandardCharsets;
19 import java.nio.file.Files;
20 import java.nio.file.Path;
21 import java.nio.file.Paths;
22 import java.nio.file.StandardOpenOption;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.Collection;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.regex.Pattern;
29 import org.gradle.internal.os.OperatingSystem;
31 /**
32 * This class provides support for the Fossil executable, to do some tasks
33 * and otherwise from within Gradle.
35 * @since 2020/06/24
37 public final class FossilExe
39 /** Cached executable. */
40 @SuppressWarnings({"StaticVariableMayNotBeInitialized", "unused"})
41 private static FossilExe _cached;
43 /** The executable path. */
44 private final Path exe;
46 /**
47 * Initializes the executable reference with the given path.
49 * @param __exe The executable path.
50 * @throws NullPointerException On null arguments.
51 * @since 2020/06/24
53 public FossilExe(Path __exe)
54 throws NullPointerException
56 if (__exe == null)
57 throw new NullPointerException("NARG");
59 this.exe = __exe;
62 /**
63 * Cats the given file name.
65 * @param __fileName The file name.
66 * @return The file data.
67 * @throws NullPointerException On null arguments.
68 * @since 2020/06/27
70 public final byte[] cat(String __fileName)
71 throws NullPointerException
73 if (__fileName == null)
74 throw new NullPointerException("NARG");
76 return this.runRawOutput("cat", __fileName);
79 /**
80 * Returns the current fossil user.
82 * @return The current fossil user.
83 * @throws InvalidFossilExeException If the user is not valid.
84 * @since 2020/06/27
86 public final String currentUser()
87 throws InvalidFossilExeException
89 // Use first line found for the user
90 for (String line : this.runLineOutput("user", "default"))
91 if (!line.isEmpty())
92 return line.trim();
94 // Fail
95 throw new InvalidFossilExeException("No default user set in Fossil, " +
96 "please set `fossil user default user.name`");
99 /**
100 * Returns the executable path.
102 * @return The executable path.
103 * @since 2020/06/25
105 public final Path exePath()
107 return this.exe;
111 * Runs the specified command and returns the process for it.
113 * @param __args Arguments to the command.
114 * @return The process of the command.
115 * @since 2020/06/24
117 @SuppressWarnings("UseOfProcessBuilder")
118 public final Process runCommand(String... __args)
120 ProcessBuilder builder = new ProcessBuilder();
122 // The first argument is always the command
123 List<String> command = new ArrayList<>();
124 command.add(this.exe.toAbsolutePath().toString());
126 // Add all subsequent arguments
127 if (__args != null)
128 command.addAll(Arrays.<String>asList(__args));
130 // Use this command
131 builder.command(command);
133 // Standard output is always printed to the console, for debugging
134 builder.redirectError(ProcessBuilder.Redirect.INHERIT);
136 // Force specific locales and otherwise
137 Map<String, String> env = builder.environment();
138 env.put("LC_ALL", "C");
140 // Start the command but wrap IOException as it is annoying
143 return builder.start();
145 catch (IOException e)
147 throw new RuntimeException("Could not execute command.", e);
152 * Executes the command and returns the lines used.
154 * @param __args The arguments to call.
155 * @return The lines used in the command.
156 * @since 2020/06/25
158 public final Collection<String> runLineOutput(String... __args)
160 // Get the raw command bytes
161 Collection<String> rv = new ArrayList<>();
162 try (BufferedReader in = new BufferedReader(new InputStreamReader(
163 new ByteArrayInputStream(this.runRawOutput(__args)),
164 StandardCharsets.UTF_8)))
166 for (;;)
168 String ln = in.readLine();
170 if (ln == null)
171 break;
173 rv.add(ln);
177 // Could no read the command result
178 catch (IOException e)
180 throw new RuntimeException("Line read/write error.", e);
183 return rv;
187 * Runs the command and returns the raw output.
189 * @param __args The commands to run.
190 * @return The raw output of the command.
191 * @since 2020/06/25
193 public final byte[] runRawOutput(String... __args)
195 // Start the Fossil process
196 Process process = this.runCommand(__args);
198 // Read in the command data
199 try (InputStream in = process.getInputStream();
200 ByteArrayOutputStream out = new ByteArrayOutputStream())
202 // Copy data
203 byte[] buf = new byte[4096];
204 for (;;)
206 int rc = in.read(buf);
208 if (rc < 0)
209 break;
211 out.write(buf, 0, rc);
214 // Wait for it to complete
215 for (;;)
218 int exitCode = process.waitFor();
219 if (0 != exitCode)
220 throw new RuntimeException(String.format(
221 "Exited %s with failure %d: %d bytes",
222 Arrays.asList(__args), exitCode,
223 out.toByteArray().length));
224 break;
226 catch (InterruptedException ignored)
230 // Use the result
231 return out.toByteArray();
234 // Could no read the command result
235 catch (IOException e)
237 throw new RuntimeException("Raw read/write error.", e);
240 // Make sure the process stops
241 finally
243 // Make sure the process is destroyed
244 process.destroy();
249 * Gets the content of the specified file.
251 * @param __fileName The file name.
252 * @return The stream for the file data or {@code null} if no such file
253 * exists or it has no content.
254 * @throws NullPointerException On null arguments.
255 * @since 2020/06/25
257 public final InputStream unversionCat(String __fileName)
258 throws NullPointerException
260 if (__fileName == null)
261 throw new NullPointerException("NARG");
263 byte[] data = this.unversionCatBytes(__fileName);
264 if (data == null)
265 return null;
267 return new ByteArrayInputStream(data);
271 * Gets the content of the specified file.
273 * @param __fileName The file name.
274 * @return The stream for the file data or {@code null} if no such file
275 * exists or it has no content.
276 * @throws NullPointerException On null arguments.
277 * @since 2020/06/25
279 public final byte[] unversionCatBytes(String __fileName)
280 throws NullPointerException
282 // If no data is available, return nothing
283 byte[] data = this.runRawOutput("unversion", "cat", __fileName);
284 if (data.length == 0)
285 return null;
287 return data;
291 * Deletes the specified file.
293 * @param __path The path to delete.
294 * @throws NullPointerException On null arguments.
295 * @since 2022/08/29
297 public void unversionDelete(String __path)
298 throws NullPointerException
300 if (__path == null)
301 throw new NullPointerException("NARG");
303 this.runLineOutput("unversion", "rm", __path);
307 * Returns the list of unversioned files.
309 * @return The list of unversioned files.
310 * @since 2020/06/25
312 public final Collection<String> unversionList()
314 return this.runLineOutput("unversion", "ls");
318 * Stores unversion bytes.
320 * @param __fileName The file name.
321 * @param __data The data to store.
322 * @throws NullPointerException On null arguments.
323 * @since 2020/06/27
325 public final void unversionStoreBytes(String __fileName, byte[] __data)
326 throws NullPointerException
328 if (__fileName == null || __data == null)
329 throw new NullPointerException("NARG");
331 // Fossil accepts files as unversioned input
332 Path tempFile = null;
335 // Setup temporary file
336 tempFile = Files.createTempFile("squirreljme-uvfile",
337 ".bin");
339 // Write data to file
340 Files.write(tempFile, __data, StandardOpenOption.WRITE,
341 StandardOpenOption.TRUNCATE_EXISTING,
342 StandardOpenOption.CREATE);
344 // Store the file data
345 this.runRawOutput("unversion", "add",
346 tempFile.toAbsolutePath().toString(),
347 "--as", __fileName);
350 // Could not write data
351 catch (IOException e)
353 throw new RuntimeException(
354 "Could not store file: " + __fileName, e);
357 // Clean out file, if it exists
358 finally
360 if (tempFile != null)
363 Files.delete(tempFile);
365 catch (IOException ignored)
372 * Stores unversion bytes.
374 * @param __fileName The file name.
375 * @param __data The data to store.
376 * @throws IOException On read errors.
377 * @throws NullPointerException On null arguments.
378 * @since 2022/08/29
380 public final void unversionStoreBytes(String __fileName,
381 InputStream __data)
382 throws IOException, NullPointerException
384 if (__fileName == null || __data == null)
385 throw new NullPointerException("NARG");
387 byte[] bytes;
388 try (ByteArrayOutputStream baos = new ByteArrayOutputStream())
390 // Copy over
391 byte[] buf = new byte[16384];
392 for (;;)
394 int rc = __data.read(buf);
396 // EOF?
397 if (rc < 0)
398 break;
400 baos.write(buf, 0, rc);
403 // Get all the bytes
404 bytes = baos.toByteArray();
407 // Forward to store call
408 this.unversionStoreBytes(__fileName, bytes);
412 * Returns the fossil version.
414 * @return The fossil version.
415 * @since 2020/06/25
417 public final FossilVersion version()
419 Collection<String> lines = this.runLineOutput("version");
421 // Try to find the fossil version
422 for (String line : lines)
424 // Ignore lines not containing this text
425 if (!line.toLowerCase().contains("this is fossil version"))
426 continue;
428 // Try to find the version number
429 for (String split : line.split("[ \t]"))
431 // Ignore blank sequences
432 if (split.isEmpty())
433 continue;
435 // Use the split that starts with a number
436 char first = split.charAt(0);
437 if (first >= '1' && first <= '9')
440 return new FossilVersion(split);
442 catch (IllegalArgumentException e)
444 throw new InvalidFossilExeException("Invalid Exe", e);
449 // This is not a good thing
450 throw new InvalidFossilExeException("Could not find fossil version.");
454 * Attempts to locate the fossil executable.
456 * @return The executable instance.
457 * @throws InvalidFossilExeException If an executable could not be found.
458 * @since 2020/06/24
460 @SuppressWarnings({"CallToSystemGetenv",
461 "StaticVariableUsedBeforeInitialization"})
462 public static FossilExe instance()
463 throws InvalidFossilExeException
465 // Pre-cached already?
466 FossilExe rv = FossilExe._cached;
467 if (rv != null)
468 return rv;
470 // Try to find it
471 Path maybe = PathUtils.findPathExecutable("fossil");
472 if (maybe == null)
473 throw new InvalidFossilExeException(
474 "Could not find Fossil executable.");
476 // Cache for later
477 rv = new FossilExe(maybe);
478 FossilExe._cached = rv;
479 return rv;
483 * Check to see if Fossil is available.
485 * @param __withUser Also check that the user is set?
486 * @return If Fossil is available.
487 * @since 2020/06/25
489 public static boolean isAvailable(boolean __withUser)
493 FossilExe exe = FossilExe.instance();
495 // These will throw exceptions if not valid
496 exe.version();
498 // Check user as well?
499 if (__withUser)
500 exe.currentUser();
502 return true;
505 // Not available
506 catch (InvalidFossilExeException ignored)
508 return false;