package processing.mode.cpp;
import java.awt.*;
import java.io.*;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.swing.*;
import processing.app.RunnerListener;
/**
* A guided, step-by-step installer for the C++ toolchain (g++) and the
* OpenGL libraries (GLFW, GLEW) that CppMode sketches need to compile and
* run, for Windows and macOS.
*
* This is the *primary* path: CppBuild calls {@link #run(RunnerListener)}
* first when it detects a missing compiler or library. If the wizard is
* cancelled, or fails, or the platform isn't Windows/macOS, CppBuild falls
* back to its existing detection/dialog logic unchanged â this class never
* replaces that fallback, only tries to resolve things more smoothly first.
*
* Windows: detects/installs MSYS2 + mingw-w64 g++/GLFW/GLEW via pacman.
* macOS: detects/installs Xcode Command Line Tools (g++/clang++), then
* GLFW/GLEW via Homebrew if Homebrew is already present.
*/
public class InstallWizard {
/**
* Thrown when the user explicitly cancels the wizard (at the initial
* missing-dependencies prompt, or mid-install). Callers should treat
* this as "stop entirely" rather than falling back to other dialogs â
* the user made a deliberate choice not to proceed.
*/
public static class CancelledByUser extends Exception {
public CancelledByUser() { super("Installation cancelled by user."); }
}
/**
* Checks what's missing and, if anything is, shows a plain error dialog
* first ("Missing: g++, glfw" + Install/Cancel). Only if the user clicks
* Install does the full progress wizard open and start downloading.
* Must be called from a background thread, not the EDT.
*
* @return true if everything needed is confirmed available by the time
* this returns; false if the user cancelled or installation
* could not be completed.
*/
public static boolean run(RunnerListener listener) throws CancelledByUser {
String os = System.getProperty("os.name").toLowerCase();
InstallWizard w = new InstallWizard();
boolean isWin = os.contains("win");
boolean isMac = os.contains("mac");
boolean isLinux = os.contains("linux") || os.contains("nix") || os.contains("nux");
java.util.List missing;
if (isWin) {
missing = w.detectWindowsMissing();
} else if (isMac) {
missing = w.detectMacMissing();
} else if (isLinux) {
missing = w.detectLinuxMissing();
} else {
// Unrecognized OS â we don't know how to install anything here, so
// just tell the user exactly what's needed and let them sort it out.
// This isn't a cancellation, so it returns false rather than
// throwing â CppBuild's caller can still decide what to do next.
java.util.List generic = new java.util.ArrayList<>();
generic.add("g++ (a C++17-capable compiler)");
generic.add("glfw");
generic.add("glew");
SwingUtilities.invokeLater(() -> JOptionPane.showMessageDialog(null,
"Couldn't recognize this operating system (" + System.getProperty("os.name") + "),\n"
+ "so C++ Mode can't install anything automatically here.\n\n"
+ "Missing: " + String.join(", ", generic) + "\n\n"
+ "Please install these manually using your system's package manager.",
"C++ Mode â Missing Dependencies",
JOptionPane.WARNING_MESSAGE));
return false;
}
if (missing.isEmpty()) return true;
final boolean[] proceed = { false };
final boolean[] answered = { false };
final Object lock = new Object();
SwingUtilities.invokeLater(() -> {
Object[] options = { "Install", "Cancel" };
int choice = JOptionPane.showOptionDialog(null,
"Missing: " + String.join(", ", missing) + "\n\n"
+ "C++ Mode needs these to compile and run sketches.",
"C++ Mode â Missing Dependencies",
JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE,
null, options, options[0]);
synchronized (lock) {
proceed[0] = (choice == 0); // index of "Install"
answered[0] = true;
lock.notifyAll();
}
});
synchronized (lock) {
while (!answered[0]) {
try { lock.wait(); } catch (InterruptedException ignored) {}
}
}
if (!proceed[0]) throw new CancelledByUser();
// User confirmed â now open the real progress wizard and install.
// A cancel from inside the wizard itself also surfaces as
// CancelledByUser (checked via w.cancelled right after each call),
// distinguishing "user backed out" from "install genuinely failed".
boolean result;
if (isWin) {
result = w.runWindows(missing);
} else if (isMac) {
result = w.runMac(missing);
} else {
result = w.runLinux(missing);
}
if (!result && w.cancelled.get()) throw new CancelledByUser();
return result;
}
// ââ Shared dialog plumbing ââââââââââââââââââââââââââââââââââââââââââââ
private JDialog dialog;
private JTextArea log;
private JLabel stepLabel;
private JButton cancelButton;
private final AtomicBoolean cancelled = new AtomicBoolean(false);
private final AtomicBoolean succeeded = new AtomicBoolean(false);
private final Object doneLock = new Object();
private boolean finished = false;
private void buildDialog(String title) {
dialog = new JDialog((Frame) null, title, true);
dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
dialog.addWindowListener(new java.awt.event.WindowAdapter() {
@Override public void windowClosing(java.awt.event.WindowEvent e) {
requestCancel();
}
});
JPanel root = new JPanel(new BorderLayout(10, 10));
root.setBorder(BorderFactory.createEmptyBorder(14, 16, 14, 16));
stepLabel = new JLabel("Preparing...");
stepLabel.setFont(stepLabel.getFont().deriveFont(Font.BOLD, 13f));
root.add(stepLabel, BorderLayout.NORTH);
log = new JTextArea(14, 56);
log.setEditable(false);
log.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
JScrollPane scroll = new JScrollPane(log);
root.add(scroll, BorderLayout.CENTER);
JPanel buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 0));
cancelButton = new JButton("Cancel");
cancelButton.addActionListener(e -> requestCancel());
buttons.add(cancelButton);
root.add(buttons, BorderLayout.SOUTH);
dialog.setContentPane(root);
dialog.pack();
dialog.setLocationRelativeTo(null);
}
private void setStep(String text) {
SwingUtilities.invokeLater(() -> stepLabel.setText(text));
}
private void appendLog(String text) {
SwingUtilities.invokeLater(() -> {
log.append(text);
if (!text.endsWith("\n")) log.append("\n");
log.setCaretPosition(log.getDocument().getLength());
});
}
private void requestCancel() {
cancelled.set(true);
finishDialog(false, "Cancelled.");
}
private void finishDialog(boolean success, String finalMessage) {
succeeded.set(success);
SwingUtilities.invokeLater(() -> {
if (finalMessage != null) setStep(finalMessage);
cancelButton.setText("Close");
});
synchronized (doneLock) {
finished = true;
doneLock.notifyAll();
}
}
/** Waits for the wizard to finish (closed, cancelled, or completed). */
private boolean waitForCompletion() {
synchronized (doneLock) {
while (!finished) {
try { doneLock.wait(); } catch (InterruptedException ignored) {}
}
}
return succeeded.get();
}
private void showDialogAndWaitForClose() {
// dialog.setVisible(true) blocks on the EDT (it's modal), so we show
// it from the EDT and let the worker thread (already running) drive
// progress via appendLog/setStep/finishDialog. Once finishDialog has
// run, we dispose the dialog so setVisible(true) returns.
SwingUtilities.invokeLater(() -> dialog.setVisible(true));
synchronized (doneLock) {
while (!finished) {
try { doneLock.wait(); } catch (InterruptedException ignored) {}
}
}
SwingUtilities.invokeLater(() -> dialog.dispose());
}
private boolean commandExists(String... cmd) {
try {
Process p = new ProcessBuilder(cmd).redirectErrorStream(true).start();
// Drain output so the process doesn't block on a full pipe.
try (InputStream is = p.getInputStream()) { is.readAllBytes(); }
return p.waitFor() == 0;
} catch (Exception e) {
return false;
}
}
// ââ Windows ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
/** Pure detection, no dialog â used by run() before showing anything. */
private java.util.List detectWindowsMissing() {
String pacman = findWindowsPacman();
boolean haveGpp = pacman != null ? false : commandExists("g++", "--version");
if (pacman == null) {
for (String p : new String[] {
"C:\\msys64\\mingw64\\bin\\g++.exe",
"C:\\msys2\\mingw64\\bin\\g++.exe",
System.getProperty("user.home") + "\\msys64\\mingw64\\bin\\g++.exe"}) {
if (new File(p).exists()) { haveGpp = true; break; }
}
}
java.util.List missing = new java.util.ArrayList<>();
if (pacman == null) {
missing.add("MSYS2");
missing.add("g++");
missing.add("glfw");
missing.add("glew");
} else {
if (!haveGpp) missing.add("g++");
boolean[] libs = windowsLibsPresentDetailed();
if (!libs[0]) missing.add("glfw");
if (!libs[1]) missing.add("glew");
}
return missing;
}
/**
* Opens the progress wizard and installs whatever's in `missing`. Only
* called after the user has already confirmed via the plain error
* dialog in run() â no further confirmation happens here.
*/
private boolean runWindows(java.util.List missing) {
buildDialog("C++ Mode Setup â Windows");
String pacman = findWindowsPacman();
Thread worker = new Thread(() -> {
setStep("Installing: " + String.join(", ", missing));
boolean ok = installWindowsToolchain(pacman);
if (cancelled.get()) return;
if (ok) {
finishDialog(true, "Setup complete! Please restart Processing to finish.");
} else {
finishDialog(false, "Install didn't finish â see the log above.");
}
});
worker.setDaemon(true);
worker.start();
showDialogAndWaitForClose();
return succeeded.get();
}
private String findWindowsPacman() {
for (String p : new String[] {
"C:\\msys64\\usr\\bin\\pacman.exe",
"C:\\msys2\\usr\\bin\\pacman.exe",
System.getProperty("user.home") + "\\msys64\\usr\\bin\\pacman.exe"}) {
if (new File(p).exists()) return p;
}
return null;
}
/** Returns {glfwPresent, glewPresent}. */
private boolean[] windowsLibsPresentDetailed() {
boolean glfw = false, glew = false;
for (String dir : new String[] {"C:\\msys64\\mingw64\\bin", "C:\\msys2\\mingw64\\bin"}) {
if (new File(dir, "glfw3.dll").exists()) glfw = true;
if (new File(dir, "glew32.dll").exists()) glew = true;
}
return new boolean[] { glfw, glew };
}
/**
* Downloads MSYS2 if needed, then installs g++/GLFW/GLEW via pacman,
* streaming progress into the wizard's log. Runs entirely on the
* calling (background) thread â no PowerShell window is spawned, so
* progress is visible directly in the wizard instead of a separate
* console.
*/
private boolean installWindowsToolchain(String existingPacman) {
try {
String pacman = existingPacman;
if (pacman == null) {
File installer = File.createTempFile("msys2-installer", ".exe");
installer.deleteOnExit();
String url =
"https://github.com/msys2/msys2-installer/releases/download/nightly-x86_64/msys2-x86_64-latest.exe";
try (InputStream in = new java.net.URI(url).toURL().openStream();
OutputStream out = new FileOutputStream(installer)) {
byte[] buf = new byte[64 * 1024];
long total = 0;
int n;
while ((n = in.read(buf)) != -1) {
if (cancelled.get()) return false;
out.write(buf, 0, n);
total += n;
if (total % (1024 * 1024) < buf.length) {
appendLog("Downloaded " + (total / (1024 * 1024)) + " MB...");
}
}
}
Process installProc = new ProcessBuilder(
installer.getAbsolutePath(), "install",
"--confirm-command", "--accept-messages", "--root", "C:/msys64")
.redirectErrorStream(true).start();
streamToLog(installProc);
installProc.waitFor();
pacman = "C:\\msys64\\usr\\bin\\pacman.exe";
if (!new File(pacman).exists()) {
appendLog("MSYS2 installation did not complete as expected.");
return false;
}
}
if (cancelled.get()) return false;
Process pacProc = new ProcessBuilder(
pacman, "-S", "--noconfirm", "--needed", "--overwrite=*",
"mingw-w64-x86_64-gcc", "mingw-w64-x86_64-glfw", "mingw-w64-x86_64-glew")
.redirectErrorStream(true).start();
streamToLog(pacProc);
int code = pacProc.waitFor();
if (code != 0) {
appendLog("pacman exited with code " + code);
return false;
}
try {
ProcessBuilder setxPb = new ProcessBuilder(
"setx", "PATH", "C:\\msys64\\mingw64\\bin;%PATH%");
setxPb.redirectErrorStream(true);
Process setx = setxPb.start();
streamToLog(setx);
setx.waitFor();
} catch (Exception e) {
appendLog("Could not update PATH automatically: " + e.getMessage());
appendLog("You may need to add C:\\msys64\\mingw64\\bin to PATH manually.");
}
return true;
} catch (Exception e) {
appendLog("Error: " + e.getMessage());
return false;
}
}
// ââ macOS ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
/** Pure detection, no dialog â used by run() before showing anything. */
private java.util.List detectMacMissing() {
boolean haveCompiler = commandExists("xcrun", "-find", "g++")
|| commandExists("g++", "--version");
boolean glfwOk = new File("/opt/homebrew/lib/libglfw.dylib").exists()
|| new File("/usr/local/lib/libglfw.dylib").exists()
|| commandExists("pkg-config", "glfw3");
boolean glewOk = new File("/opt/homebrew/lib/libGLEW.dylib").exists()
|| new File("/usr/local/lib/libGLEW.dylib").exists()
|| commandExists("pkg-config", "glew");
java.util.List missing = new java.util.ArrayList<>();
if (!haveCompiler) missing.add("g++ (Xcode Command Line Tools)");
if (!glfwOk) missing.add("glfw");
if (!glewOk) missing.add("glew");
return missing;
}
/**
* Opens the progress wizard and installs whatever's in `missing`. Only
* called after the user has already confirmed via the plain error
* dialog in run() â no further confirmation happens here.
*/
private boolean runMac(java.util.List missing) {
buildDialog("C++ Mode Setup â macOS");
boolean needsCompiler = missing.contains("g++ (Xcode Command Line Tools)");
java.util.List missingLibs = new java.util.ArrayList<>();
if (missing.contains("glfw")) missingLibs.add("glfw");
if (missing.contains("glew")) missingLibs.add("glew");
Thread worker = new Thread(() -> {
if (needsCompiler) {
setStep("Installing: g++ (Xcode Command Line Tools)");
try {
new ProcessBuilder("xcode-select", "--install").start();
} catch (Exception e) {
appendLog("Could not launch the installer: " + e.getMessage());
finishDialog(false, "Couldn't start the Xcode Command Line Tools installer.");
return;
}
// Poll for completion since xcode-select --install returns
// immediately while the GUI installer runs separately.
boolean installed = false;
for (int i = 0; i < 180; i++) { // up to ~15 minutes
if (cancelled.get()) return;
try { Thread.sleep(5000); } catch (InterruptedException ignored) {}
if (commandExists("xcrun", "-find", "g++")) { installed = true; break; }
}
if (!installed) {
finishDialog(false,
"Still waiting on Xcode Command Line Tools â finish the install, then try again.");
return;
}
}
if (cancelled.get()) return;
if (missingLibs.isEmpty()) {
finishDialog(true, "Setup complete.");
return;
}
if (!commandExists("brew", "--version")) {
appendLog("Missing: " + String.join(", ", missingLibs)
+ " â install Homebrew from https://brew.sh, then run:");
appendLog(" brew install " + String.join(" ", missingLibs));
finishDialog(false, "g++ is ready, but " + String.join(", ", missingLibs)
+ " still need Homebrew â see log above.");
return;
}
setStep("Installing: " + String.join(", ", missingLibs));
try {
java.util.List brewCmd = new java.util.ArrayList<>();
brewCmd.add("brew"); brewCmd.add("install");
brewCmd.addAll(missingLibs);
Process p = new ProcessBuilder(brewCmd).redirectErrorStream(true).start();
streamToLog(p);
int code = p.waitFor();
if (cancelled.get()) return;
if (code == 0) {
finishDialog(true, "Setup complete.");
} else {
finishDialog(false, "Homebrew install exited with code " + code + " â see log above.");
}
} catch (Exception e) {
appendLog("Error: " + e.getMessage());
finishDialog(false, "Couldn't run Homebrew â see log above.");
}
});
worker.setDaemon(true);
worker.start();
showDialogAndWaitForClose();
return succeeded.get();
}
// ââ Linux ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
// Package manager -> install command, matching CppBuild's existing
// mapping exactly so the wizard and the fallback dialog never disagree
// about what gets installed.
private static final java.util.LinkedHashMap LINUX_MANAGERS = new java.util.LinkedHashMap<>();
static {
LINUX_MANAGERS.put("apt-get", new String[]{"apt-get","install","-y","g++","libglfw3-dev","libglew-dev"});
LINUX_MANAGERS.put("apt", new String[]{"apt","install","-y","g++","libglfw3-dev","libglew-dev"});
LINUX_MANAGERS.put("pacman", new String[]{"pacman","-S","--noconfirm","gcc","glfw-x11","glew"});
LINUX_MANAGERS.put("dnf", new String[]{"dnf","install","-y","gcc-c++","glfw-devel","glew-devel"});
LINUX_MANAGERS.put("yum", new String[]{"yum","install","-y","gcc-c++","glfw-devel","glew-devel"});
LINUX_MANAGERS.put("zypper", new String[]{"zypper","install","-y","gcc-c++","glfw-devel","glew-devel"});
LINUX_MANAGERS.put("emerge", new String[]{"emerge","media-libs/glfw","media-libs/glew","sys-devel/gcc"});
LINUX_MANAGERS.put("nix-env", new String[]{"nix-env","-iA","nixpkgs.gcc","nixpkgs.glfw","nixpkgs.glew"});
}
/** Pure detection, no dialog â used by run() before showing anything. */
private java.util.List detectLinuxMissing() {
boolean haveGpp = commandExists("g++", "--version");
boolean glfwOk = commandExists("pkg-config", "--exists", "glfw3")
|| new File("/usr/lib/libglfw.so").exists()
|| new File("/usr/lib/x86_64-linux-gnu/libglfw.so.3").exists();
boolean glewOk = commandExists("pkg-config", "--exists", "glew")
|| new File("/usr/lib/libGLEW.so").exists()
|| new File("/usr/lib/x86_64-linux-gnu/libGLEW.so").exists();
java.util.List missing = new java.util.ArrayList<>();
if (!haveGpp) missing.add("g++");
if (!glfwOk) missing.add("glfw");
if (!glewOk) missing.add("glew");
return missing;
}
private String detectLinuxPackageManager() {
for (String pm : LINUX_MANAGERS.keySet()) {
if (commandExists("which", pm)) return pm;
}
return null;
}
/**
* Opens the progress wizard and installs whatever's in `missing`. Only
* called after the user has already confirmed via the plain error
* dialog in run() â no further confirmation happens here.
*
* Unlike Windows/macOS, Linux package managers need root, and there's
* no clean way to capture a sudo password prompt inside our own log
* box â so this launches a real terminal (same approach CppBuild's
* existing fallback already uses) rather than streaming output here.
*/
private boolean runLinux(java.util.List missing) {
buildDialog("C++ Mode Setup â Linux");
String pm = detectLinuxPackageManager();
Thread worker = new Thread(() -> {
if (pm == null) {
appendLog("Couldn't detect a supported package manager.");
appendLog("Missing: " + String.join(", ", missing));
appendLog("Please install these manually, for example:");
appendLog(" sudo apt install g++ libglfw3-dev libglew-dev (Debian/Ubuntu)");
appendLog(" sudo pacman -S gcc glfw-x11 glew (Arch)");
appendLog(" sudo dnf install gcc-c++ glfw-devel glew-devel (Fedora)");
finishDialog(false, "Couldn't detect a package manager â see log above.");
return;
}
String[] pmCmd = LINUX_MANAGERS.get(pm);
setStep("Installing: " + String.join(", ", missing) + " (via " + pm + ")");
appendLog("A terminal window will open to run:");
appendLog(" sudo " + String.join(" ", pmCmd));
appendLog("Enter your password there if prompted, then return here.");
boolean launched = launchLinuxTerminalInstall(pmCmd);
if (cancelled.get()) return;
if (!launched) {
finishDialog(false,
"Couldn't launch a terminal â run the command above manually.");
return;
}
finishDialog(true,
"Installer launched in a separate terminal. Once it finishes, restart Processing.");
});
worker.setDaemon(true);
worker.start();
showDialogAndWaitForClose();
return succeeded.get();
}
private boolean launchLinuxTerminalInstall(String[] pmCmd) {
String cmdStr = "sudo " + String.join(" ", pmCmd);
String[] terminals = {
"x-terminal-emulator", "gnome-terminal", "konsole",
"xfce4-terminal", "mate-terminal", "xterm", "alacritty",
"kitty", "tilix", "terminator"
};
try {
File tmp = File.createTempFile("cpp_install_", ".sh");
tmp.deleteOnExit();
tmp.setExecutable(true);
try (PrintWriter pw = new PrintWriter(tmp)) {
pw.println("#!/bin/bash");
pw.println("echo 'Installing g++, GLFW, GLEW...'");
pw.println(cmdStr);
pw.println("echo ''");
pw.println("echo 'Done! Please restart Processing4.'");
pw.println("read -p 'Press Enter to close...'");
}
for (String term : terminals) {
if (!commandExists("which", term)) continue;
try {
ProcessBuilder pb;
if (term.equals("gnome-terminal")) {
pb = new ProcessBuilder(term, "--", "bash", tmp.getAbsolutePath());
} else if (term.equals("konsole") || term.equals("kitty") || term.equals("alacritty")) {
pb = new ProcessBuilder(term, "-e", "bash", tmp.getAbsolutePath());
} else {
pb = new ProcessBuilder(term, "-e", "bash " + tmp.getAbsolutePath());
}
pb.start();
return true;
} catch (Exception ignored) {}
}
// No terminal found â try pkexec (graphical sudo) as a last resort.
try {
new ProcessBuilder("pkexec", "bash", tmp.getAbsolutePath()).start();
return true;
} catch (Exception ignored) {}
return false;
} catch (Exception e) {
appendLog("Error: " + e.getMessage());
return false;
}
}
// ââ Shared helpers âââââââââââââââââââââââââââââââââââââââââââââââââââââ
private void streamToLog(Process p) throws IOException {
try (BufferedReader br = new BufferedReader(
new InputStreamReader(p.getInputStream()))) {
String line;
while ((line = br.readLine()) != null) {
if (cancelled.get()) {
p.destroy();
break;
}
appendLog(line);
}
}
}
}