···11+package app.build;
22+33+import java.io.IOException;
44+import java.util.concurrent.CompletableFuture;
55+66+/**
77+ * Interface for building Paper Mario decomp projects.
88+ * Implementations handle different build environments (native Nix, WSL+NixOS).
99+ */
1010+public interface BuildEnvironment
1111+{
1212+ /**
1313+ * Returns a human-readable name for this environment type.
1414+ */
1515+ String getName();
1616+1717+ /**
1818+ * Runs configure (./configure).
1919+ * @param listener Callback for real-time build output
2020+ * @return The result of the configure operation
2121+ * @throws BuildException If the build environment is not properly set up
2222+ * @throws IOException If an I/O error occurs
2323+ */
2424+ BuildResult configure(BuildOutputListener listener) throws BuildException, IOException;
2525+2626+ /**
2727+ * Builds the project (ninja).
2828+ * @param listener Callback for real-time build output
2929+ * @return The result of the build operation
3030+ * @throws BuildException If the build environment is not properly set up
3131+ * @throws IOException If an I/O error occurs
3232+ */
3333+ BuildResult build(BuildOutputListener listener) throws BuildException, IOException;
3434+3535+ /**
3636+ * Cleans the build directory (./configure --clean).
3737+ * @param listener Callback for real-time build output
3838+ * @return The result of the clean operation
3939+ * @throws BuildException If the build environment is not properly set up
4040+ * @throws IOException If an I/O error occurs
4141+ */
4242+ BuildResult clean(BuildOutputListener listener) throws BuildException, IOException;
4343+4444+ /**
4545+ * Builds the project asynchronously.
4646+ * @param listener Callback for real-time build output
4747+ * @return A CompletableFuture that completes with the build result
4848+ */
4949+ CompletableFuture<BuildResult> buildAsync(BuildOutputListener listener);
5050+5151+ /**
5252+ * Cancels any running build operation.
5353+ * @return True if a build was cancelled, false if no build was running
5454+ */
5555+ boolean cancel();
5656+5757+ /**
5858+ * Returns whether a build is currently in progress.
5959+ */
6060+ boolean isBuilding();
6161+}
+41
src/main/java/app/build/BuildException.java
···11+package app.build;
22+33+/**
44+ * Exception thrown when a build operation fails.
55+ */
66+public class BuildException extends Exception
77+{
88+ private static final long serialVersionUID = 1L;
99+1010+ private final boolean silent;
1111+1212+ public BuildException(String message)
1313+ {
1414+ this(message, null, false);
1515+ }
1616+1717+ public BuildException(String message, Throwable cause)
1818+ {
1919+ this(message, cause, false);
2020+ }
2121+2222+ public BuildException(String message, boolean silent)
2323+ {
2424+ this(message, null, silent);
2525+ }
2626+2727+ public BuildException(String message, Throwable cause, boolean silent)
2828+ {
2929+ super(message, cause);
3030+ this.silent = silent;
3131+ }
3232+3333+ /**
3434+ * Returns true if this exception should not be displayed to the user as an error.
3535+ * Used for graceful cancellations.
3636+ */
3737+ public boolean isSilent()
3838+ {
3939+ return silent;
4040+ }
4141+}
+51
src/main/java/app/build/BuildOutputListener.java
···11+package app.build;
22+33+import util.Logger;
44+55+/**
66+ * Callback interface for receiving real-time build output.
77+ */
88+@FunctionalInterface
99+public interface BuildOutputListener
1010+{
1111+ /**
1212+ * Called when a line of output is received from the build process.
1313+ * @param line The output line (stdout or stderr)
1414+ * @param isError True if this line came from stderr
1515+ */
1616+ void onOutput(String line, boolean isError);
1717+1818+ /**
1919+ * Creates a listener that logs all output to the Logger.
2020+ */
2121+ static BuildOutputListener toLogger()
2222+ {
2323+ return (line, isError) -> {
2424+ if (isError) {
2525+ Logger.logError(line);
2626+ }
2727+ else {
2828+ Logger.log(line);
2929+ }
3030+ };
3131+ }
3232+3333+ /**
3434+ * Creates a listener that discards all output.
3535+ */
3636+ static BuildOutputListener silent()
3737+ {
3838+ return (line, isError) -> {};
3939+ }
4040+4141+ /**
4242+ * Combines this listener with another, calling both for each output line.
4343+ */
4444+ default BuildOutputListener andThen(BuildOutputListener other)
4545+ {
4646+ return (line, isError) -> {
4747+ this.onOutput(line, isError);
4848+ other.onOutput(line, isError);
4949+ };
5050+ }
5151+}
+78
src/main/java/app/build/BuildResult.java
···11+package app.build;
22+33+import java.io.File;
44+import java.time.Duration;
55+import java.util.Optional;
66+77+/**
88+ * Contains the result of a build operation.
99+ */
1010+public class BuildResult
1111+{
1212+ public enum Status
1313+ {
1414+ SUCCESS,
1515+ FAILURE,
1616+ CANCELLED
1717+ }
1818+1919+ private final Status status;
2020+ private final int exitCode;
2121+ private final Duration duration;
2222+ private final File outputRom;
2323+ private final String errorMessage;
2424+2525+ private BuildResult(Status status, int exitCode, Duration duration, File outputRom, String errorMessage)
2626+ {
2727+ this.status = status;
2828+ this.exitCode = exitCode;
2929+ this.duration = duration;
3030+ this.outputRom = outputRom;
3131+ this.errorMessage = errorMessage;
3232+ }
3333+3434+ public static BuildResult success(int exitCode, Duration duration, File outputRom)
3535+ {
3636+ return new BuildResult(Status.SUCCESS, exitCode, duration, outputRom, null);
3737+ }
3838+3939+ public static BuildResult failure(int exitCode, Duration duration, String errorMessage)
4040+ {
4141+ return new BuildResult(Status.FAILURE, exitCode, duration, null, errorMessage);
4242+ }
4343+4444+ public static BuildResult cancelled(Duration duration)
4545+ {
4646+ return new BuildResult(Status.CANCELLED, -1, duration, null, "Build was cancelled");
4747+ }
4848+4949+ public Status getStatus()
5050+ {
5151+ return status;
5252+ }
5353+5454+ public boolean isSuccess()
5555+ {
5656+ return status == Status.SUCCESS;
5757+ }
5858+5959+ public int getExitCode()
6060+ {
6161+ return exitCode;
6262+ }
6363+6464+ public Duration getDuration()
6565+ {
6666+ return duration;
6767+ }
6868+6969+ public Optional<File> getOutputRom()
7070+ {
7171+ return Optional.ofNullable(outputRom);
7272+ }
7373+7474+ public Optional<String> getErrorMessage()
7575+ {
7676+ return Optional.ofNullable(errorMessage);
7777+ }
7878+}