this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

add 'Build Project' button

+1379
+276
src/main/java/app/BuildOutputDialog.java
··· 1 + package app; 2 + 3 + import java.awt.Color; 4 + import java.awt.Dimension; 5 + import java.awt.Frame; 6 + import java.io.IOException; 7 + import java.util.regex.Matcher; 8 + import java.util.regex.Pattern; 9 + 10 + import javax.swing.JButton; 11 + import javax.swing.JDialog; 12 + import javax.swing.JProgressBar; 13 + import javax.swing.JScrollPane; 14 + import javax.swing.JTextArea; 15 + import javax.swing.ScrollPaneConstants; 16 + import javax.swing.SwingUtilities; 17 + import javax.swing.WindowConstants; 18 + import javax.swing.text.DefaultCaret; 19 + 20 + import app.build.BuildEnvironment; 21 + import app.build.BuildException; 22 + import app.build.BuildOutputListener; 23 + import app.build.BuildResult; 24 + import app.build.NixEnvironment; 25 + import app.build.WslNixOsEnvironment; 26 + import net.miginfocom.swing.MigLayout; 27 + 28 + /** 29 + * Dialog that displays build output in real-time with a progress bar. 30 + */ 31 + public class BuildOutputDialog extends JDialog 32 + { 33 + public static final String NINJA_STATUS = "NINJA %P "; // Percentage time remaining estimate 34 + private static final Pattern NINJA_PROGRESS = Pattern.compile("NINJA +(\\d+)% (.*)"); 35 + 36 + private final JTextArea outputArea; 37 + private final JProgressBar progressBar; 38 + private final JButton cancelButton; 39 + private final JButton closeButton; 40 + 41 + private BuildEnvironment buildEnv; 42 + private boolean buildComplete = false; 43 + 44 + public BuildOutputDialog(Frame parent) 45 + { 46 + super(parent, "Building...", false); 47 + 48 + setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); 49 + addWindowListener(new java.awt.event.WindowAdapter() { 50 + @Override 51 + public void windowClosing(java.awt.event.WindowEvent e) 52 + { 53 + handleClose(); 54 + } 55 + }); 56 + 57 + outputArea = new JTextArea(); 58 + outputArea.setEditable(false); 59 + outputArea.setFont(new java.awt.Font(java.awt.Font.MONOSPACED, java.awt.Font.PLAIN, 12)); 60 + outputArea.setRows(25); 61 + outputArea.setColumns(100); 62 + 63 + // Enable auto-scroll 64 + DefaultCaret caret = (DefaultCaret) outputArea.getCaret(); 65 + caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE); 66 + 67 + JScrollPane scrollPane = new JScrollPane(outputArea); 68 + scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); 69 + scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS); 70 + 71 + progressBar = new JProgressBar(0, 100); 72 + progressBar.setIndeterminate(true); 73 + progressBar.setStringPainted(true); 74 + progressBar.setString("Configuring..."); 75 + 76 + cancelButton = new JButton("Cancel"); 77 + cancelButton.addActionListener(e -> handleCancel()); 78 + 79 + closeButton = new JButton("Close"); 80 + closeButton.setEnabled(false); 81 + closeButton.addActionListener(e -> dispose()); 82 + 83 + setLayout(new MigLayout("fill, ins 8", "[grow]", "[grow][pref!][pref!]")); 84 + add(scrollPane, "grow, wrap"); 85 + add(progressBar, "growx, wrap 8"); 86 + add(cancelButton, "split 2, align right"); 87 + add(closeButton, "align right"); 88 + 89 + pack(); 90 + setMinimumSize(new Dimension(600, 400)); 91 + setLocationRelativeTo(parent); 92 + } 93 + 94 + /** 95 + * Starts the build process asynchronously. 96 + */ 97 + public void startBuild() 98 + { 99 + setVisible(true); 100 + 101 + // Create build environment on background thread 102 + Environment.getExecutor().submit(() -> { 103 + try { 104 + if (Environment.isWindows()) { 105 + buildEnv = new WslNixOsEnvironment(); 106 + } 107 + else { 108 + buildEnv = new NixEnvironment(); 109 + } 110 + 111 + try { 112 + buildEnv.configure(getOutputListener()); 113 + } 114 + catch (IOException | BuildException e) { 115 + SwingUtilities.invokeLater(() -> { 116 + appendOutput("Configuration error: " + e.getMessage(), true); 117 + handleBuildError(e); 118 + }); 119 + return; 120 + } 121 + 122 + buildEnv.buildAsync(getOutputListener()).thenAccept(result -> { 123 + SwingUtilities.invokeLater(() -> handleBuildComplete(result)); 124 + }).exceptionally(ex -> { 125 + SwingUtilities.invokeLater(() -> handleBuildError(ex)); 126 + return null; 127 + }); 128 + } 129 + catch (BuildException e) { 130 + SwingUtilities.invokeLater(() -> { 131 + if (!e.isSilent()) { 132 + appendOutput("Build environment error: " + e.getMessage(), true); 133 + handleBuildError(e); 134 + } 135 + else { 136 + appendOutput(e.getMessage(), false); 137 + handleBuildComplete(BuildResult.cancelled(java.time.Duration.ZERO)); 138 + } 139 + }); 140 + } 141 + }); 142 + } 143 + 144 + private BuildOutputListener getOutputListener() 145 + { 146 + return (line, isError) -> { 147 + SwingUtilities.invokeLater(() -> { 148 + if (!parseProgress(line)) { 149 + appendOutput(line, isError); 150 + } 151 + }); 152 + }; 153 + } 154 + 155 + private void appendOutput(String line, boolean isError) 156 + { 157 + if (isError) { 158 + // For errors, we could style differently but JTextArea doesn't support that easily 159 + outputArea.append(line + "\n"); 160 + } 161 + else { 162 + outputArea.append(line + "\n"); 163 + } 164 + } 165 + 166 + private boolean parseProgress(String line) 167 + { 168 + Matcher m = NINJA_PROGRESS.matcher(line); 169 + if (m.find()) { 170 + int percent = Integer.parseInt(m.group(1)); 171 + String description = m.group(2); 172 + 173 + progressBar.setIndeterminate(false); 174 + progressBar.setValue(percent); 175 + progressBar.setString(description); 176 + 177 + return true; 178 + } 179 + return false; 180 + } 181 + 182 + private void handleBuildComplete(BuildResult result) 183 + { 184 + buildComplete = true; 185 + cancelButton.setEnabled(false); 186 + closeButton.setEnabled(true); 187 + progressBar.setIndeterminate(false); 188 + 189 + if (buildEnv != null) { 190 + buildEnv = null; 191 + } 192 + 193 + switch (result.getStatus()) { 194 + case SUCCESS: 195 + setTitle("Build Complete"); 196 + progressBar.setValue(100); 197 + progressBar.setString("Build Successful"); 198 + progressBar.setForeground(new Color(0, 150, 0)); 199 + 200 + result.getOutputRom().ifPresent(rom -> { 201 + appendOutput("\n=== Build completed successfully ===", false); 202 + appendOutput("ROM: " + rom.getAbsolutePath(), false); 203 + }); 204 + break; 205 + 206 + case FAILURE: 207 + setTitle("Build Failed"); 208 + progressBar.setValue(0); 209 + progressBar.setString("Build Failed"); 210 + progressBar.setForeground(Color.RED); 211 + 212 + result.getErrorMessage().ifPresent(msg -> { 213 + appendOutput("\n=== Build failed ===", true); 214 + appendOutput(msg, true); 215 + }); 216 + break; 217 + 218 + case CANCELLED: 219 + setTitle("Build Cancelled"); 220 + progressBar.setValue(0); 221 + progressBar.setString("Cancelled"); 222 + appendOutput("\n=== Build cancelled ===", false); 223 + break; 224 + } 225 + } 226 + 227 + private void handleBuildError(Throwable ex) 228 + { 229 + buildComplete = true; 230 + cancelButton.setEnabled(false); 231 + closeButton.setEnabled(true); 232 + progressBar.setIndeterminate(false); 233 + progressBar.setValue(0); 234 + progressBar.setString("Error"); 235 + progressBar.setForeground(Color.RED); 236 + 237 + if (buildEnv != null) { 238 + buildEnv = null; 239 + } 240 + 241 + setTitle("Build Error"); 242 + appendOutput("\n=== Build error ===", true); 243 + appendOutput(ex.getMessage(), true); 244 + 245 + if (!(ex instanceof BuildException) || !((BuildException) ex).isSilent()) { 246 + SwingUtils.getErrorDialog() 247 + .setTitle("Build Error") 248 + .setMessage(ex.getMessage()) 249 + .show(); 250 + } 251 + } 252 + 253 + private void handleCancel() 254 + { 255 + if (buildEnv != null && !buildComplete) { 256 + appendOutput("\nCancelling build...", false); 257 + buildEnv.cancel(); 258 + } 259 + } 260 + 261 + private void handleClose() 262 + { 263 + if (!buildComplete && buildEnv != null) { 264 + handleCancel(); 265 + } 266 + else { 267 + dispose(); 268 + } 269 + } 270 + 271 + @Override 272 + public void dispose() 273 + { 274 + super.dispose(); 275 + } 276 + }
+28
src/main/java/app/Environment.java
··· 101 101 102 102 private static boolean isDeluxe = false; 103 103 104 + private static final List<Runnable> shutdownHooks = new ArrayList<>(); 105 + 104 106 private static String versionString; 105 107 private static String gitBuildBranch; 106 108 private static String gitBuildCommit; ··· 270 272 271 273 public static void exit(int status) 272 274 { 275 + runShutdownHooks(); 273 276 System.exit(status); 277 + } 278 + 279 + /** 280 + * Adds a hook to be run when the application exits via Environment.exit(). 281 + */ 282 + public static void addShutdownHook(Runnable hook) 283 + { 284 + synchronized (shutdownHooks) { 285 + shutdownHooks.add(hook); 286 + } 287 + } 288 + 289 + private static void runShutdownHooks() 290 + { 291 + synchronized (shutdownHooks) { 292 + for (Runnable hook : shutdownHooks) { 293 + try { 294 + hook.run(); 295 + } 296 + catch (Exception e) { 297 + Logger.logError("Exception in shutdown hook: " + e.getMessage()); 298 + Logger.printStackTrace(e); 299 + } 300 + } 301 + } 274 302 } 275 303 276 304 public static boolean isCommandLine()
+54
src/main/java/app/StarRodMain.java
··· 38 38 39 39 import org.apache.commons.io.FilenameUtils; 40 40 41 + import app.build.BuildEnvironment; 42 + import app.build.BuildOutputListener; 43 + import app.build.BuildResult; 44 + import app.build.NixEnvironment; 45 + import app.build.WslNixOsEnvironment; 41 46 import app.config.Options; 42 47 import app.input.InvalidInputException; 43 48 import assets.AssetHandle; ··· 193 198 }); 194 199 buttons.add(extractDataButton); 195 200 201 + JButton buildProjectButton = new JButton("Build Project"); 202 + trySetIcon(buildProjectButton, ExpectedAsset.ICON_GOLD); 203 + SwingUtils.setFontSize(buildProjectButton, 12); 204 + buildProjectButton.addActionListener((e) -> { 205 + action_buildProject(); 206 + }); 207 + buttons.add(buildProjectButton); 208 + 196 209 // not ready 197 210 /* 198 211 JButton captureThumbnailsButton = new JButton("Capture Thumbnails"); ··· 297 310 add(openConfigDirButton, "grow"); 298 311 add(openProjectDirButton, "grow"); 299 312 313 + add(buildProjectButton, "grow, span, gaptop 8"); 314 + 300 315 add(progressPanel, "grow, span, wrap, gap top 8"); 301 316 add(consoleScrollPane, "grow, span"); 302 317 ··· 437 452 .show(); 438 453 } 439 454 }); 455 + } 456 + 457 + private void action_buildProject() 458 + { 459 + BuildOutputDialog dialog = new BuildOutputDialog(this); 460 + dialog.startBuild(); 440 461 } 441 462 442 463 private void action_captureThumbnails() ··· 624 645 } 625 646 catch (IOException e) { 626 647 Logger.printStackTrace(e); 648 + } 649 + break; 650 + 651 + case "-BUILDPROJECT": 652 + BuildEnvironment env = null; 653 + try { 654 + if (Environment.isWindows()) { 655 + env = new WslNixOsEnvironment(); 656 + } 657 + else { 658 + env = new NixEnvironment(); 659 + } 660 + 661 + BuildResult result = env.configure(BuildOutputListener.toLogger()); 662 + if (result.isSuccess()) { 663 + result = env.build(BuildOutputListener.toLogger()); 664 + } 665 + 666 + if (!result.isSuccess()) { 667 + throw new StarRodException("Build failed: " + result.getErrorMessage().orElse("unknown")); 668 + } 669 + Logger.log("ROM built: " + result.getOutputRom().get()); 670 + } 671 + catch (app.build.BuildException e) { 672 + if (!e.isSilent()) { 673 + Logger.logError("Build environment error: " + e.getMessage()); 674 + } 675 + else { 676 + Logger.log(e.getMessage()); 677 + } 678 + } 679 + catch (IOException e) { 680 + Logger.logError("Build environment error: " + e.getMessage()); 627 681 } 628 682 break; 629 683
+61
src/main/java/app/build/BuildEnvironment.java
··· 1 + package app.build; 2 + 3 + import java.io.IOException; 4 + import java.util.concurrent.CompletableFuture; 5 + 6 + /** 7 + * Interface for building Paper Mario decomp projects. 8 + * Implementations handle different build environments (native Nix, WSL+NixOS). 9 + */ 10 + public interface BuildEnvironment 11 + { 12 + /** 13 + * Returns a human-readable name for this environment type. 14 + */ 15 + String getName(); 16 + 17 + /** 18 + * Runs configure (./configure). 19 + * @param listener Callback for real-time build output 20 + * @return The result of the configure operation 21 + * @throws BuildException If the build environment is not properly set up 22 + * @throws IOException If an I/O error occurs 23 + */ 24 + BuildResult configure(BuildOutputListener listener) throws BuildException, IOException; 25 + 26 + /** 27 + * Builds the project (ninja). 28 + * @param listener Callback for real-time build output 29 + * @return The result of the build operation 30 + * @throws BuildException If the build environment is not properly set up 31 + * @throws IOException If an I/O error occurs 32 + */ 33 + BuildResult build(BuildOutputListener listener) throws BuildException, IOException; 34 + 35 + /** 36 + * Cleans the build directory (./configure --clean). 37 + * @param listener Callback for real-time build output 38 + * @return The result of the clean operation 39 + * @throws BuildException If the build environment is not properly set up 40 + * @throws IOException If an I/O error occurs 41 + */ 42 + BuildResult clean(BuildOutputListener listener) throws BuildException, IOException; 43 + 44 + /** 45 + * Builds the project asynchronously. 46 + * @param listener Callback for real-time build output 47 + * @return A CompletableFuture that completes with the build result 48 + */ 49 + CompletableFuture<BuildResult> buildAsync(BuildOutputListener listener); 50 + 51 + /** 52 + * Cancels any running build operation. 53 + * @return True if a build was cancelled, false if no build was running 54 + */ 55 + boolean cancel(); 56 + 57 + /** 58 + * Returns whether a build is currently in progress. 59 + */ 60 + boolean isBuilding(); 61 + }
+41
src/main/java/app/build/BuildException.java
··· 1 + package app.build; 2 + 3 + /** 4 + * Exception thrown when a build operation fails. 5 + */ 6 + public class BuildException extends Exception 7 + { 8 + private static final long serialVersionUID = 1L; 9 + 10 + private final boolean silent; 11 + 12 + public BuildException(String message) 13 + { 14 + this(message, null, false); 15 + } 16 + 17 + public BuildException(String message, Throwable cause) 18 + { 19 + this(message, cause, false); 20 + } 21 + 22 + public BuildException(String message, boolean silent) 23 + { 24 + this(message, null, silent); 25 + } 26 + 27 + public BuildException(String message, Throwable cause, boolean silent) 28 + { 29 + super(message, cause); 30 + this.silent = silent; 31 + } 32 + 33 + /** 34 + * Returns true if this exception should not be displayed to the user as an error. 35 + * Used for graceful cancellations. 36 + */ 37 + public boolean isSilent() 38 + { 39 + return silent; 40 + } 41 + }
+51
src/main/java/app/build/BuildOutputListener.java
··· 1 + package app.build; 2 + 3 + import util.Logger; 4 + 5 + /** 6 + * Callback interface for receiving real-time build output. 7 + */ 8 + @FunctionalInterface 9 + public interface BuildOutputListener 10 + { 11 + /** 12 + * Called when a line of output is received from the build process. 13 + * @param line The output line (stdout or stderr) 14 + * @param isError True if this line came from stderr 15 + */ 16 + void onOutput(String line, boolean isError); 17 + 18 + /** 19 + * Creates a listener that logs all output to the Logger. 20 + */ 21 + static BuildOutputListener toLogger() 22 + { 23 + return (line, isError) -> { 24 + if (isError) { 25 + Logger.logError(line); 26 + } 27 + else { 28 + Logger.log(line); 29 + } 30 + }; 31 + } 32 + 33 + /** 34 + * Creates a listener that discards all output. 35 + */ 36 + static BuildOutputListener silent() 37 + { 38 + return (line, isError) -> {}; 39 + } 40 + 41 + /** 42 + * Combines this listener with another, calling both for each output line. 43 + */ 44 + default BuildOutputListener andThen(BuildOutputListener other) 45 + { 46 + return (line, isError) -> { 47 + this.onOutput(line, isError); 48 + other.onOutput(line, isError); 49 + }; 50 + } 51 + }
+78
src/main/java/app/build/BuildResult.java
··· 1 + package app.build; 2 + 3 + import java.io.File; 4 + import java.time.Duration; 5 + import java.util.Optional; 6 + 7 + /** 8 + * Contains the result of a build operation. 9 + */ 10 + public class BuildResult 11 + { 12 + public enum Status 13 + { 14 + SUCCESS, 15 + FAILURE, 16 + CANCELLED 17 + } 18 + 19 + private final Status status; 20 + private final int exitCode; 21 + private final Duration duration; 22 + private final File outputRom; 23 + private final String errorMessage; 24 + 25 + private BuildResult(Status status, int exitCode, Duration duration, File outputRom, String errorMessage) 26 + { 27 + this.status = status; 28 + this.exitCode = exitCode; 29 + this.duration = duration; 30 + this.outputRom = outputRom; 31 + this.errorMessage = errorMessage; 32 + } 33 + 34 + public static BuildResult success(int exitCode, Duration duration, File outputRom) 35 + { 36 + return new BuildResult(Status.SUCCESS, exitCode, duration, outputRom, null); 37 + } 38 + 39 + public static BuildResult failure(int exitCode, Duration duration, String errorMessage) 40 + { 41 + return new BuildResult(Status.FAILURE, exitCode, duration, null, errorMessage); 42 + } 43 + 44 + public static BuildResult cancelled(Duration duration) 45 + { 46 + return new BuildResult(Status.CANCELLED, -1, duration, null, "Build was cancelled"); 47 + } 48 + 49 + public Status getStatus() 50 + { 51 + return status; 52 + } 53 + 54 + public boolean isSuccess() 55 + { 56 + return status == Status.SUCCESS; 57 + } 58 + 59 + public int getExitCode() 60 + { 61 + return exitCode; 62 + } 63 + 64 + public Duration getDuration() 65 + { 66 + return duration; 67 + } 68 + 69 + public Optional<File> getOutputRom() 70 + { 71 + return Optional.ofNullable(outputRom); 72 + } 73 + 74 + public Optional<String> getErrorMessage() 75 + { 76 + return Optional.ofNullable(errorMessage); 77 + } 78 + }
+145
src/main/java/app/build/NixEnvironment.java
··· 1 + package app.build; 2 + 3 + import java.io.File; 4 + import java.io.IOException; 5 + import java.util.concurrent.CompletableFuture; 6 + 7 + import app.BuildOutputDialog; 8 + import app.Environment; 9 + 10 + /** 11 + * Build environment implementation for native Nix (Linux/macOS). 12 + * Runs commands via `nix develop -c bash -c "<command>"`. 13 + */ 14 + public class NixEnvironment implements BuildEnvironment 15 + { 16 + private static final String ROM_PATH = "ver/us/build/papermario.z64"; 17 + 18 + private final ProcessRunner runner = new ProcessRunner(); 19 + 20 + @Override 21 + public String getName() 22 + { 23 + return "Nix"; 24 + } 25 + 26 + @Override 27 + public BuildResult configure(BuildOutputListener listener) throws BuildException, IOException 28 + { 29 + validateEnvironment(); 30 + return runNixCommand("./configure", listener); 31 + } 32 + 33 + @Override 34 + public BuildResult build(BuildOutputListener listener) throws BuildException, IOException 35 + { 36 + validateEnvironment(); 37 + ProcessRunner.ProcessResult result = runNixCommandRaw("NINJA_STATUS='" + BuildOutputDialog.NINJA_STATUS + "' ninja", listener); 38 + 39 + if (result.wasCancelled()) { 40 + return BuildResult.cancelled(result.getDuration()); 41 + } 42 + 43 + File rom = new File(Environment.getProjectDirectory(), ROM_PATH); 44 + if (result.isSuccess() && rom.exists()) { 45 + return BuildResult.success(result.getExitCode(), result.getDuration(), rom); 46 + } 47 + else { 48 + String error = result.getExitCode() == 0 ? "ROM file not found" : "Build failed with exit code " + result.getExitCode(); 49 + return BuildResult.failure(result.getExitCode(), result.getDuration(), error); 50 + } 51 + } 52 + 53 + @Override 54 + public BuildResult clean(BuildOutputListener listener) throws BuildException, IOException 55 + { 56 + validateEnvironment(); 57 + return runNixCommand("./configure --clean", listener); 58 + } 59 + 60 + @Override 61 + public CompletableFuture<BuildResult> buildAsync(BuildOutputListener listener) 62 + { 63 + return CompletableFuture.supplyAsync(() -> { 64 + try { 65 + return build(listener); 66 + } 67 + catch (BuildException | IOException e) { 68 + return BuildResult.failure(-1, java.time.Duration.ZERO, e.getMessage()); 69 + } 70 + }, Environment.getExecutor()); 71 + } 72 + 73 + @Override 74 + public boolean cancel() 75 + { 76 + return runner.cancel(); 77 + } 78 + 79 + @Override 80 + public boolean isBuilding() 81 + { 82 + return runner.isRunning(); 83 + } 84 + 85 + private void validateEnvironment() throws BuildException 86 + { 87 + // Check for nix binary 88 + if (!isNixInstalled()) { 89 + throw new BuildException( 90 + "Nix is not installed. Please install Nix from https://nixos.org/download.html\n" + 91 + "Run: curl -L https://nixos.org/nix/install | sh -s -- --daemon"); 92 + } 93 + 94 + // Check for flake.nix in project directory 95 + File projectDir = Environment.getProjectDirectory(); 96 + if (projectDir == null) { 97 + throw new BuildException("No project directory is set"); 98 + } 99 + 100 + File flakeFile = new File(projectDir, "flake.nix"); 101 + if (!flakeFile.exists()) { 102 + throw new BuildException("No flake.nix found in project directory: " + projectDir.getAbsolutePath()); 103 + } 104 + } 105 + 106 + private boolean isNixInstalled() 107 + { 108 + try { 109 + ProcessBuilder pb = new ProcessBuilder("which", "nix"); 110 + Process process = pb.start(); 111 + int exitCode = process.waitFor(); 112 + return exitCode == 0; 113 + } 114 + catch (IOException | InterruptedException e) { 115 + return false; 116 + } 117 + } 118 + 119 + private BuildResult runNixCommand(String command, BuildOutputListener listener) throws IOException 120 + { 121 + ProcessRunner.ProcessResult result = runNixCommandRaw(command, listener); 122 + 123 + if (result.wasCancelled()) { 124 + return BuildResult.cancelled(result.getDuration()); 125 + } 126 + else if (result.isSuccess()) { 127 + return BuildResult.success(result.getExitCode(), result.getDuration(), null); 128 + } 129 + else { 130 + return BuildResult.failure(result.getExitCode(), result.getDuration(), 131 + "Command failed with exit code " + result.getExitCode()); 132 + } 133 + } 134 + 135 + private ProcessRunner.ProcessResult runNixCommandRaw(String command, BuildOutputListener listener) throws IOException 136 + { 137 + File projectDir = Environment.getProjectDirectory(); 138 + 139 + String[] cmd = new String[] { 140 + "nix", "develop", "-c", "bash", "-c", command 141 + }; 142 + 143 + return runner.run(cmd, projectDir, listener); 144 + } 145 + }
+148
src/main/java/app/build/ProcessRunner.java
··· 1 + package app.build; 2 + 3 + import java.io.BufferedReader; 4 + import java.io.File; 5 + import java.io.IOException; 6 + import java.io.InputStreamReader; 7 + import java.time.Duration; 8 + import java.time.Instant; 9 + import java.util.concurrent.atomic.AtomicBoolean; 10 + import java.util.concurrent.atomic.AtomicReference; 11 + 12 + /** 13 + * Utility class for running external processes with output streaming. 14 + */ 15 + public class ProcessRunner 16 + { 17 + private final AtomicReference<Process> currentProcess = new AtomicReference<>(); 18 + private final AtomicBoolean cancelled = new AtomicBoolean(false); 19 + 20 + /** 21 + * Runs a command and streams output to the listener. 22 + * @param command The command to run 23 + * @param workingDir The working directory, or null to use current directory 24 + * @param listener Callback for output lines 25 + * @return The result of running the command 26 + * @throws IOException If an I/O error occurs 27 + */ 28 + public ProcessResult run(String[] command, File workingDir, BuildOutputListener listener) throws IOException 29 + { 30 + cancelled.set(false); 31 + Instant startTime = Instant.now(); 32 + 33 + ProcessBuilder pb = new ProcessBuilder(command); 34 + if (workingDir != null) { 35 + pb.directory(workingDir); 36 + } 37 + pb.redirectErrorStream(false); 38 + 39 + Process process = pb.start(); 40 + currentProcess.set(process); 41 + 42 + Thread stdoutThread = new Thread(() -> { 43 + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { 44 + String line; 45 + while ((line = reader.readLine()) != null) { 46 + listener.onOutput(line, false); 47 + } 48 + } 49 + catch (IOException e) { 50 + // Process was likely destroyed 51 + } 52 + }, "ProcessRunner-stdout"); 53 + 54 + Thread stderrThread = new Thread(() -> { 55 + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { 56 + String line; 57 + while ((line = reader.readLine()) != null) { 58 + listener.onOutput(line, true); 59 + } 60 + } 61 + catch (IOException e) { 62 + // Process was likely destroyed 63 + } 64 + }, "ProcessRunner-stderr"); 65 + 66 + stdoutThread.start(); 67 + stderrThread.start(); 68 + 69 + try { 70 + int exitCode = process.waitFor(); 71 + stdoutThread.join(); 72 + stderrThread.join(); 73 + 74 + Duration duration = Duration.between(startTime, Instant.now()); 75 + return new ProcessResult(exitCode, duration, cancelled.get()); 76 + } 77 + catch (InterruptedException e) { 78 + Thread.currentThread().interrupt(); 79 + process.destroyForcibly(); 80 + Duration duration = Duration.between(startTime, Instant.now()); 81 + return new ProcessResult(-1, duration, true); 82 + } 83 + finally { 84 + currentProcess.set(null); 85 + } 86 + } 87 + 88 + /** 89 + * Cancels the currently running process. 90 + * @return True if a process was cancelled, false if no process was running 91 + */ 92 + public boolean cancel() 93 + { 94 + Process process = currentProcess.get(); 95 + if (process != null && process.isAlive()) { 96 + cancelled.set(true); 97 + process.destroyForcibly(); 98 + return true; 99 + } 100 + return false; 101 + } 102 + 103 + /** 104 + * Returns whether a process is currently running. 105 + */ 106 + public boolean isRunning() 107 + { 108 + Process process = currentProcess.get(); 109 + return process != null && process.isAlive(); 110 + } 111 + 112 + /** 113 + * Result of running a process. 114 + */ 115 + public static class ProcessResult 116 + { 117 + private final int exitCode; 118 + private final Duration duration; 119 + private final boolean cancelled; 120 + 121 + public ProcessResult(int exitCode, Duration duration, boolean cancelled) 122 + { 123 + this.exitCode = exitCode; 124 + this.duration = duration; 125 + this.cancelled = cancelled; 126 + } 127 + 128 + public int getExitCode() 129 + { 130 + return exitCode; 131 + } 132 + 133 + public Duration getDuration() 134 + { 135 + return duration; 136 + } 137 + 138 + public boolean wasCancelled() 139 + { 140 + return cancelled; 141 + } 142 + 143 + public boolean isSuccess() 144 + { 145 + return !cancelled && exitCode == 0; 146 + } 147 + } 148 + }
+497
src/main/java/app/build/WslNixOsEnvironment.java
··· 1 + package app.build; 2 + 3 + import java.io.BufferedReader; 4 + import java.io.File; 5 + import java.io.IOException; 6 + import java.io.InputStream; 7 + import java.io.InputStreamReader; 8 + import java.net.URI; 9 + import java.net.URL; 10 + import java.nio.file.Files; 11 + import java.nio.file.Path; 12 + import java.nio.file.StandardCopyOption; 13 + import java.util.concurrent.CompletableFuture; 14 + 15 + import javax.swing.JOptionPane; 16 + 17 + import app.BuildOutputDialog; 18 + import app.Environment; 19 + import app.SwingUtils; 20 + import util.Logger; 21 + 22 + /** 23 + * Build environment implementation for Windows using WSL with NixOS. 24 + * Uses a dedicated WSL distro named "StarRod-NixOS". 25 + */ 26 + public class WslNixOsEnvironment implements BuildEnvironment 27 + { 28 + private static final String DISTRO_NAME = "StarRod-NixOS"; 29 + private static final String ROM_PATH = "ver/us/build/papermario.z64"; 30 + private static final String NIXOS_WSL_RELEASE_URL = 31 + "https://github.com/nix-community/NixOS-WSL/releases/latest/download/nixos-wsl.tar.gz"; 32 + 33 + private final ProcessRunner runner = new ProcessRunner(); 34 + private boolean shutdownRegistered = false; 35 + 36 + private static final int MIN_WINDOWS_BUILD = 19041; // Windows 10 version 2004 37 + 38 + public WslNixOsEnvironment() throws BuildException 39 + { 40 + validateSystemRequirements(); 41 + validateWslSupport(); 42 + registerShutdownHook(); 43 + } 44 + 45 + @Override 46 + public String getName() 47 + { 48 + return "WSL NixOS"; 49 + } 50 + 51 + @Override 52 + public BuildResult configure(BuildOutputListener listener) throws BuildException, IOException 53 + { 54 + ensureDistroExists(listener); 55 + return runWslCommand("./configure", listener); 56 + } 57 + 58 + @Override 59 + public BuildResult build(BuildOutputListener listener) throws BuildException, IOException 60 + { 61 + ensureDistroExists(listener); 62 + ProcessRunner.ProcessResult result = runWslCommandRaw("NINJA_STATUS='" + BuildOutputDialog.NINJA_STATUS + "' ./configure && ninja", listener); 63 + 64 + if (result.wasCancelled()) { 65 + return BuildResult.cancelled(result.getDuration()); 66 + } 67 + 68 + File rom = new File(Environment.getProjectDirectory(), ROM_PATH); 69 + if (result.isSuccess() && rom.exists()) { 70 + return BuildResult.success(result.getExitCode(), result.getDuration(), rom); 71 + } 72 + else { 73 + String error = result.getExitCode() == 0 ? "ROM file not found" : "Build failed with exit code " + result.getExitCode(); 74 + return BuildResult.failure(result.getExitCode(), result.getDuration(), error); 75 + } 76 + } 77 + 78 + @Override 79 + public BuildResult clean(BuildOutputListener listener) throws BuildException, IOException 80 + { 81 + ensureDistroExists(listener); 82 + return runWslCommand("./configure --clean", listener); 83 + } 84 + 85 + @Override 86 + public CompletableFuture<BuildResult> buildAsync(BuildOutputListener listener) 87 + { 88 + return CompletableFuture.supplyAsync(() -> { 89 + try { 90 + return build(listener); 91 + } 92 + catch (BuildException | IOException e) { 93 + return BuildResult.failure(-1, java.time.Duration.ZERO, e.getMessage()); 94 + } 95 + }, Environment.getExecutor()); 96 + } 97 + 98 + @Override 99 + public boolean cancel() 100 + { 101 + return runner.cancel(); 102 + } 103 + 104 + @Override 105 + public boolean isBuilding() 106 + { 107 + return runner.isRunning(); 108 + } 109 + 110 + private void validateSystemRequirements() throws BuildException 111 + { 112 + validateWindowsVersion(); 113 + validateVirtualizationSupport(); 114 + } 115 + 116 + private void validateWindowsVersion() throws BuildException 117 + { 118 + try { 119 + ProcessBuilder pb = new ProcessBuilder( 120 + "powershell", "-NoProfile", "-Command", 121 + "[System.Environment]::OSVersion.Version.Build" 122 + ); 123 + pb.redirectErrorStream(true); 124 + Process process = pb.start(); 125 + 126 + String output; 127 + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { 128 + output = reader.readLine(); 129 + } 130 + 131 + int exitCode = process.waitFor(); 132 + if (exitCode != 0 || output == null) { 133 + Logger.logWarning("Could not determine Windows build number, proceeding anyway"); 134 + return; 135 + } 136 + 137 + int buildNumber = Integer.parseInt(output.trim()); 138 + if (buildNumber < MIN_WINDOWS_BUILD) { 139 + throw new BuildException( 140 + "Building requires Windows 10 version 2004+ (Build 19041+) or Windows 11.\n" + 141 + "Your system is running Build " + buildNumber + ".\n" + 142 + "Please update Windows to build."); 143 + } 144 + } 145 + catch (NumberFormatException e) { 146 + Logger.logWarning("Could not parse Windows build number, proceeding anyway"); 147 + } 148 + catch (IOException | InterruptedException e) { 149 + Logger.logWarning("Could not check Windows version: " + e.getMessage()); 150 + } 151 + } 152 + 153 + private void validateVirtualizationSupport() throws BuildException 154 + { 155 + try { 156 + // Check if virtualization is enabled using PowerShell and WMI 157 + ProcessBuilder pb = new ProcessBuilder( 158 + "powershell", "-NoProfile", "-Command", 159 + "(Get-CimInstance -ClassName Win32_ComputerSystem).HypervisorPresent" 160 + ); 161 + pb.redirectErrorStream(true); 162 + Process process = pb.start(); 163 + 164 + String output; 165 + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { 166 + output = reader.readLine(); 167 + } 168 + 169 + int exitCode = process.waitFor(); 170 + if (exitCode != 0 || output == null) { 171 + // Try alternative check using systeminfo 172 + checkVirtualizationViaSysteminfo(); 173 + return; 174 + } 175 + 176 + output = output.trim(); 177 + if ("True".equalsIgnoreCase(output)) { 178 + return; 179 + } 180 + 181 + // Hypervisor not present - check if CPU supports virtualization 182 + checkVirtualizationViaSysteminfo(); 183 + } 184 + catch (IOException | InterruptedException e) { 185 + Logger.logWarning("Could not check virtualization status: " + e.getMessage()); 186 + } 187 + } 188 + 189 + private void checkVirtualizationViaSysteminfo() throws BuildException 190 + { 191 + try { 192 + ProcessBuilder pb = new ProcessBuilder("systeminfo"); 193 + pb.redirectErrorStream(true); 194 + Process process = pb.start(); 195 + 196 + boolean vmMonitorExtensions = false; 197 + boolean virtualizationEnabled = false; 198 + boolean vmFirmwareEnabled = false; 199 + 200 + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { 201 + String line; 202 + while ((line = reader.readLine()) != null) { 203 + // Look for Hyper-V requirements section 204 + if (line.contains("VM Monitor Mode Extensions:")) { 205 + vmMonitorExtensions = line.toLowerCase().contains("yes"); 206 + } 207 + else if (line.contains("Virtualization Enabled In Firmware:")) { 208 + vmFirmwareEnabled = line.toLowerCase().contains("yes"); 209 + } 210 + else if (line.contains("Second Level Address Translation:") || 211 + line.contains("Hyper-V")) { 212 + if (line.toLowerCase().contains("yes")) { 213 + virtualizationEnabled = true; 214 + } 215 + } 216 + } 217 + } 218 + 219 + process.waitFor(); 220 + 221 + if (!vmMonitorExtensions) { 222 + throw new BuildException( 223 + "Your CPU does not support virtualization (VT-x/AMD-V).\n" + 224 + "Building requires a CPU with virtualization extensions.\n" + 225 + "Building is not available on your hardware."); 226 + } 227 + 228 + if (!vmFirmwareEnabled && !virtualizationEnabled) { 229 + throw new BuildException( 230 + "Virtualization is not enabled in your BIOS/UEFI settings.\n" + 231 + "Building requires virtualization to be enabled.\n" + 232 + "Please restart your computer, enter BIOS/UEFI setup,\n" + 233 + "and enable Intel VT-x or AMD-V virtualization."); 234 + } 235 + } 236 + catch (IOException | InterruptedException e) { 237 + Logger.logWarning("Could not verify virtualization via systeminfo: " + e.getMessage()); 238 + } 239 + } 240 + 241 + private void validateWslSupport() throws BuildException 242 + { 243 + try { 244 + ProcessBuilder pb = new ProcessBuilder("wsl", "--status"); 245 + pb.redirectErrorStream(true); 246 + Process process = pb.start(); 247 + int exitCode = process.waitFor(); 248 + 249 + if (exitCode != 0) { 250 + offerWslInstallation(); 251 + } 252 + } 253 + catch (IOException e) { 254 + offerWslInstallation(); 255 + } 256 + catch (InterruptedException e) { 257 + Thread.currentThread().interrupt(); 258 + throw new BuildException("Interrupted while checking WSL status"); 259 + } 260 + } 261 + 262 + private void offerWslInstallation() throws BuildException 263 + { 264 + if (Environment.isCommandLine()) { 265 + throw new BuildException( 266 + "WSL is not installed on this system.\n" + 267 + "Please enable WSL by running 'wsl --install' in an administrator PowerShell."); 268 + } 269 + 270 + int choice = SwingUtils.getConfirmDialog() 271 + .setTitle("WSL Not Installed") 272 + .setMessage( 273 + "WSL (Windows Subsystem for Linux) is required to build.", 274 + "Would you like to install it now?", 275 + "Note: This requires administrator privileges and a restart.") 276 + .setMessageType(JOptionPane.QUESTION_MESSAGE) 277 + .setOptionsType(JOptionPane.YES_NO_OPTION) 278 + .choose(); 279 + 280 + if (choice != JOptionPane.YES_OPTION) { 281 + throw new BuildException("WSL installation declined by user.", true); 282 + } 283 + 284 + try { 285 + Logger.log("Installing WSL..."); 286 + 287 + // Run wsl --install --no-distribution which requires admin privileges 288 + // Using cmd /c start to trigger UAC prompt 289 + ProcessBuilder pb = new ProcessBuilder( 290 + "cmd", "/c", "start", "/wait", "wsl", "--install", "--no-distribution" 291 + ); 292 + pb.redirectErrorStream(true); 293 + Process process = pb.start(); 294 + 295 + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { 296 + String line; 297 + while ((line = reader.readLine()) != null) { 298 + Logger.log(line); 299 + } 300 + } 301 + 302 + int exitCode = process.waitFor(); 303 + 304 + if (exitCode != 0) { 305 + SwingUtils.getErrorDialog() 306 + .setTitle("WSL Installation Failed") 307 + .setMessage( 308 + "WSL installation failed (exit code " + exitCode + ").", 309 + "Please try running 'wsl --install' manually in an administrator PowerShell.") 310 + .show(); 311 + throw new BuildException( 312 + "WSL installation failed. Please try running 'wsl --install' manually."); 313 + } 314 + 315 + // Installation succeeded, ask about restart 316 + int restartChoice = SwingUtils.getConfirmDialog() 317 + .setTitle("Restart Required") 318 + .setMessage( 319 + "WSL has been installed successfully.", 320 + "A restart is required to complete the installation.", 321 + "Would you like to restart your computer now?") 322 + .setMessageType(JOptionPane.QUESTION_MESSAGE) 323 + .setOptionsType(JOptionPane.YES_NO_OPTION) 324 + .choose(); 325 + 326 + if (restartChoice == JOptionPane.YES_OPTION) { 327 + Logger.log("Restarting computer..."); 328 + Runtime.getRuntime().exec("shutdown /r /t 0"); 329 + throw new BuildException("Restarting computer to complete WSL installation.", true); 330 + } 331 + else { 332 + throw new BuildException( 333 + "Please restart your computer to complete WSL installation, then try again.", true); 334 + } 335 + } 336 + catch (IOException | InterruptedException e) { 337 + throw new BuildException( 338 + "Failed to install WSL: " + e.getMessage() + "\n" + 339 + "Please try running 'wsl --install' manually in an administrator PowerShell."); 340 + } 341 + } 342 + 343 + private void registerShutdownHook() 344 + { 345 + if (!shutdownRegistered) { 346 + Environment.addShutdownHook(this::terminateDistro); 347 + shutdownRegistered = true; 348 + } 349 + } 350 + 351 + private void ensureDistroExists(BuildOutputListener listener) throws BuildException, IOException 352 + { 353 + if (isDistroInstalled()) { 354 + return; 355 + } 356 + 357 + Logger.log("NixOS-WSL distro not found, installing..."); 358 + listener.onOutput("Installing NixOS-WSL distro (this may take a few minutes)...", false); 359 + 360 + // Download NixOS-WSL tarball 361 + Path tempDir = Files.createTempDirectory("starrod-nixos-wsl"); 362 + Path tarball = tempDir.resolve("nixos-wsl.tar.gz"); 363 + 364 + try { 365 + listener.onOutput("Downloading NixOS-WSL...", false); 366 + downloadFile(NIXOS_WSL_RELEASE_URL, tarball); 367 + 368 + // Create install directory 369 + File installDir = new File(Environment.getUserStateDir(), "nixos-wsl"); 370 + installDir.mkdirs(); 371 + 372 + // Import the distro 373 + listener.onOutput("Importing WSL distro...", false); 374 + ProcessBuilder pb = new ProcessBuilder( 375 + "wsl", "--import", DISTRO_NAME, installDir.getAbsolutePath(), tarball.toString() 376 + ); 377 + pb.redirectErrorStream(true); 378 + Process process = pb.start(); 379 + 380 + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { 381 + String line; 382 + while ((line = reader.readLine()) != null) { 383 + listener.onOutput(line, false); 384 + } 385 + } 386 + 387 + int exitCode = process.waitFor(); 388 + if (exitCode != 0) { 389 + throw new BuildException("Failed to import NixOS-WSL distro (exit code " + exitCode + ")"); 390 + } 391 + 392 + listener.onOutput("NixOS-WSL distro installed successfully!", false); 393 + } 394 + catch (InterruptedException e) { 395 + Thread.currentThread().interrupt(); 396 + throw new BuildException("Interrupted while installing NixOS-WSL"); 397 + } 398 + finally { 399 + // Clean up temp files 400 + Files.deleteIfExists(tarball); 401 + Files.deleteIfExists(tempDir); 402 + } 403 + } 404 + 405 + private boolean isDistroInstalled() 406 + { 407 + try { 408 + ProcessBuilder pb = new ProcessBuilder("wsl", "-l", "-q"); 409 + pb.redirectErrorStream(true); 410 + Process process = pb.start(); 411 + 412 + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { 413 + String line; 414 + while ((line = reader.readLine()) != null) { 415 + // WSL output may contain null characters 416 + line = line.replace("\0", "").trim(); 417 + if (DISTRO_NAME.equals(line)) { 418 + return true; 419 + } 420 + } 421 + } 422 + 423 + process.waitFor(); 424 + return false; 425 + } 426 + catch (IOException | InterruptedException e) { 427 + return false; 428 + } 429 + } 430 + 431 + private void downloadFile(String urlString, Path destination) throws IOException 432 + { 433 + URL url = URI.create(urlString).toURL(); 434 + try (InputStream in = url.openStream()) { 435 + Files.copy(in, destination, StandardCopyOption.REPLACE_EXISTING); 436 + } 437 + } 438 + 439 + private void terminateDistro() 440 + { 441 + if (!isDistroInstalled()) { 442 + return; 443 + } 444 + 445 + try { 446 + Logger.log("Terminating WSL distro: " + DISTRO_NAME); 447 + ProcessBuilder pb = new ProcessBuilder("wsl", "--terminate", DISTRO_NAME); 448 + pb.redirectErrorStream(true); 449 + Process process = pb.start(); 450 + process.waitFor(); 451 + } 452 + catch (IOException | InterruptedException e) { 453 + Logger.logError("Failed to terminate WSL distro: " + e.getMessage()); 454 + } 455 + } 456 + 457 + private String convertToWslPath(File windowsPath) 458 + { 459 + String absPath = windowsPath.getAbsolutePath(); 460 + // Convert C:\foo\bar to /mnt/c/foo/bar 461 + if (absPath.length() >= 2 && absPath.charAt(1) == ':') { 462 + char driveLetter = Character.toLowerCase(absPath.charAt(0)); 463 + String rest = absPath.substring(2).replace('\\', '/'); 464 + return "/mnt/" + driveLetter + rest; 465 + } 466 + return absPath.replace('\\', '/'); 467 + } 468 + 469 + private BuildResult runWslCommand(String command, BuildOutputListener listener) throws IOException 470 + { 471 + ProcessRunner.ProcessResult result = runWslCommandRaw(command, listener); 472 + 473 + if (result.wasCancelled()) { 474 + return BuildResult.cancelled(result.getDuration()); 475 + } 476 + else if (result.isSuccess()) { 477 + return BuildResult.success(result.getExitCode(), result.getDuration(), null); 478 + } 479 + else { 480 + return BuildResult.failure(result.getExitCode(), result.getDuration(), 481 + "Command failed with exit code " + result.getExitCode()); 482 + } 483 + } 484 + 485 + private ProcessRunner.ProcessResult runWslCommandRaw(String command, BuildOutputListener listener) throws IOException 486 + { 487 + File projectDir = Environment.getProjectDirectory(); 488 + String wslPath = convertToWslPath(projectDir); 489 + 490 + String[] cmd = new String[] { 491 + "wsl", "-d", DISTRO_NAME, "--cd", wslPath, 492 + "nix", "develop", "-c", "bash", "-c", command 493 + }; 494 + 495 + return runner.run(cmd, null, listener); 496 + } 497 + }