package processing.mode.cpp;
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.regex.*;
import processing.app.RunnerListener;
import processing.app.Sketch;
public class CppBuild {
private final Sketch sketch;
private final CppMode mode;
final File buildDir;
private final File runtimeDir;
public int headerLineCount = 0;
private static final Pattern ERR_LINE = Pattern.compile(
"(?:Sketch_run\\.cpp|sketch\\.pde):(\\d+):\\d+:\\s+error:");
private static final Pattern FUNC_DEF = Pattern.compile(
"^[ \\t]*(void|float|int|double|bool|char|long|color|auto|PVector|PImage\\s*\\*?|std::string)" +
"\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(([^)]*)\\)\\s*\\{",
Pattern.MULTILINE);
private static final Set LIFECYCLE = new HashSet<>(Arrays.asList(
"setup", "draw", "settings",
"mousePressed", "mouseReleased", "mouseClicked",
"mouseMoved", "mouseDragged", "mouseWheel",
"keyPressed", "keyReleased", "keyTyped",
"windowMoved", "windowResized"));
private static final String[] RESERVED = {
"rand", "time", "exit", "abs", "min", "max", "log", "exp", "pow"
};
// Processing.h defines these class/struct names in namespace Processing.
// If the user also defines them, we skip the user's version to avoid
// "redefinition" errors -- the engine's definition takes precedence.
// Processing API names that are dangerous as user variable names (Problem 5.2/6.1)
// Processing API names confirmed to cause crashes/errors when used as variable names
// Kept minimal to avoid false positives -- only add names with confirmed crash reports
private static final java.util.Set PROCESSING_API_NAMES = java.util.Set.of(
"color" // confirmed: Processing::color typedef causes operator++ crash
);
private static final java.util.Set RESERVED_TYPES = java.util.Set.of(
"Array", "ArrayList", "IntList", "FloatList", "StringList",
"PVector", "PImage", "PGraphics", "PShape", "PFont", "PShader",
"Table", "TableRow", "XML", "JSONValue", "color",
"IntDict", "FloatDict", "StringDict",
"BufferedReader", "PrintWriter"
);
public CppBuild(Sketch sketch, CppMode mode) {
this.sketch = sketch;
this.mode = mode;
this.buildDir = sketch.makeTempFolder();
this.runtimeDir = mode != null ? mode.getRuntimeDir() : null;
}
private void showGppDialog(String html, String btn, String url,
RunnerListener listener) throws Exception {
Object[] opts = { btn, "Cancel" };
int ch = javax.swing.JOptionPane.showOptionDialog(null,
new javax.swing.JLabel(html), "C++ Compiler Not Found",
javax.swing.JOptionPane.YES_NO_OPTION,
javax.swing.JOptionPane.ERROR_MESSAGE, null, opts, opts[0]);
if (ch == 0) {
try { java.awt.Desktop.getDesktop().browse(new java.net.URI(url)); }
catch (Exception ignored) {}
}
listener.statusError("g++ not found â see dialog for install instructions");
throw new Exception("g++ not found");
}
private void autoInstallWindows(RunnerListener listener) {
String script =
"$ErrorActionPreference = 'Stop'\n"
+ "Write-Host '=== C++ Mode Auto-Installer ==='\n"
+ "Write-Host ''\n"
+ "$msys2Dirs = @('C:\\msys64','C:\\msys2','C:\\tools\\msys64',"
+ "\"$env:USERPROFILE\\msys64\",\"$env:LOCALAPPDATA\\msys64\")\n"
+ "$pacman = $null\n"
+ "foreach ($dir in $msys2Dirs) {\n"
+ " $p = \"$dir\\usr\\bin\\pacman.exe\"\n"
+ " if (Test-Path $p) { $pacman = $p; Write-Host \"Found MSYS2 at $dir\"; break }\n"
+ "}\n"
+ "if (-not $pacman) {\n"
+ " $inPath = Get-Command pacman -ErrorAction SilentlyContinue\n"
+ " if ($inPath) { $pacman = $inPath.Source; Write-Host \"Found pacman on PATH\" }\n"
+ "}\n"
+ "if (-not $pacman) {\n"
+ " Write-Host 'Downloading MSYS2 installer (this may take a minute)...'\n"
+ " $url = 'https://github.com/msys2/msys2-installer/releases/download/nightly-x86_64/msys2-x86_64-latest.exe'\n"
+ " $dest = \"$env:TEMP\\msys2-installer.exe\"\n"
+ " $wc = New-Object System.Net.WebClient\n"
+ " $wc.add_DownloadProgressChanged({\n"
+ " param($s,$e)\n"
+ " $pct = $e.ProgressPercentage\n"
+ " $bar = '#' * [int]($pct/2)\n"
+ " $empty = '-' * (50 - [int]($pct/2))\n"
+ " Write-Host -NoNewline \"\r [$bar$empty] $pct% \"\n"
+ " })\n"
+ " $done = $false\n"
+ " $wc.add_DownloadFileCompleted({ $done = $true })\n"
+ " $wc.DownloadFileAsync([uri]$url, $dest)\n"
+ " while (-not $done) { Start-Sleep -Milliseconds 100 }\n"
+ " Write-Host ''\n"
+ " Write-Host 'Running MSYS2 installer...'\n"
+ " Start-Process -Wait $dest -ArgumentList 'install','--confirm-command','--accept-messages','--root','C:/msys64'\n"
+ " $pacman = 'C:\\msys64\\usr\\bin\\pacman.exe'\n"
+ " Write-Host 'MSYS2 installed.'\n"
+ "}\n"
+ "Write-Host ''\n"
+ "Write-Host 'Installing g++, GLFW, GLEW...'\n"
+ "& $pacman -S --noconfirm --needed '--overwrite=*' mingw-w64-x86_64-gcc mingw-w64-x86_64-glfw mingw-w64-x86_64-glew\n"
+ "Write-Host 'Adding MSYS2 to system PATH...'\n"
+ "$mingwBin = 'C:\\msys64\\mingw64\\bin'\n"
+ "$currentPath = [Environment]::GetEnvironmentVariable('PATH','Machine')\n"
+ "if ($currentPath -notlike \"*$mingwBin*\") {\n"
+ " [Environment]::SetEnvironmentVariable('PATH', \"$mingwBin;$currentPath\", 'Machine')\n"
+ " Write-Host 'Added to system PATH.'\n"
+ "} else { Write-Host 'Already in PATH.' }\n"
+ "Write-Host ''\n"
+ "Write-Host '============================================'\n"
+ "Write-Host 'Installation complete!'\n"
+ "Write-Host 'Please restart Processing4 to use C++ Mode.'\n"
+ "Write-Host '============================================'\n"
+ "Read-Host 'Press Enter to close'\n";
try {
java.io.File tmp = java.io.File.createTempFile("cpp_install_", ".ps1");
tmp.deleteOnExit();
try (java.io.PrintWriter pw = new java.io.PrintWriter(tmp)) { pw.print(script); }
listener.statusNotice("Launching installer...");
new Thread(() -> {
try {
// Run as admin so PATH can be updated system-wide
ProcessBuilder pb2 = new ProcessBuilder(
"powershell.exe", "-ExecutionPolicy", "Bypass",
"-Command",
"Start-Process powershell -Verb RunAs -ArgumentList "
+ "'-ExecutionPolicy Bypass -NoExit -File \"" + tmp.getAbsolutePath().replace("\\", "\\\\") + "\"'");
pb2.start();
javax.swing.JOptionPane.showMessageDialog(null,
new javax.swing.JLabel(
"Installer launched!
"
+ "A PowerShell window is installing g++, GLFW, and GLEW.
"
+ "When it says Installation complete!:
"
+ " 1. Close the installer window
"
+ " 2. Restart Processing4"),
"Installing...", javax.swing.JOptionPane.INFORMATION_MESSAGE);
} catch (Exception e2) { listener.statusError("Error: "+e2.getMessage()); }
}).start();
} catch (Exception e) { listener.statusError("Error: "+e.getMessage()); }
}
private void autoInstallLinux(String[] pmCmd, RunnerListener listener) {
// Build a shell script that runs the package manager with sudo
// and opens in a visible terminal so the user can enter their password
String cmdStr = "sudo " + String.join(" ", pmCmd);
// Try common terminal emulators
String[] terminals = {
"x-terminal-emulator", "gnome-terminal", "konsole",
"xfce4-terminal", "mate-terminal", "xterm", "alacritty",
"kitty", "tilix", "terminator"
};
new Thread(() -> {
try {
// Write a small shell script
java.io.File tmp = java.io.File.createTempFile("cpp_install_", ".sh");
tmp.deleteOnExit();
tmp.setExecutable(true);
try (java.io.PrintWriter pw = new java.io.PrintWriter(tmp)) {
pw.println("#!/bin/bash");
pw.println("echo '=== C++ Mode Auto-Installer ==='");
pw.println("echo ''");
pw.println("echo 'Installing g++, GLFW, GLEW...'");
pw.println(cmdStr);
pw.println("echo ''");
pw.println("echo '============================================'");
pw.println("echo 'Done! Please restart Processing4.'");
pw.println("echo '============================================'");
pw.println("read -p 'Press Enter to close...'");
}
boolean launched = false;
for (String term : terminals) {
try {
Process which = Runtime.getRuntime().exec(new String[]{"which", term});
if (which.waitFor() != 0) continue;
ProcessBuilder pb2;
if (term.equals("gnome-terminal")) {
pb2 = new ProcessBuilder(term, "--", "bash", tmp.getAbsolutePath());
} else if (term.equals("konsole")) {
pb2 = new ProcessBuilder(term, "-e", "bash", tmp.getAbsolutePath());
} else if (term.equals("kitty") || term.equals("alacritty")) {
pb2 = new ProcessBuilder(term, "-e", "bash", tmp.getAbsolutePath());
} else {
pb2 = new ProcessBuilder(term, "-e", "bash " + tmp.getAbsolutePath());
}
pb2.start();
launched = true;
break;
} catch (Exception ignored) {}
}
if (!launched) {
// Fallback: try pkexec (graphical sudo) without terminal
try {
new ProcessBuilder("pkexec", "bash", tmp.getAbsolutePath()).start();
launched = true;
} catch (Exception ignored) {}
}
if (launched) {
javax.swing.JOptionPane.showMessageDialog(null,
new javax.swing.JLabel(
"Installer launched!
"
+ "A terminal window is installing g++, GLFW, and GLEW.
"
+ "You may need to enter your password.
"
+ "When it says Done!:
"
+ " 1. Close the terminal
"
+ " 2. Restart Processing4"),
"Installing...", javax.swing.JOptionPane.INFORMATION_MESSAGE);
} else {
javax.swing.JOptionPane.showMessageDialog(null,
new javax.swing.JLabel(
"Could not launch terminal.
"
+ "Please run manually:
"
+ " " + cmdStr + "
"
+ "Then restart Processing4."),
"Manual Install Required", javax.swing.JOptionPane.WARNING_MESSAGE);
}
} catch (Exception e) { listener.statusError("Error: " + e.getMessage()); }
}).start();
}
private void checkGppAvailable(RunnerListener listener) throws Exception {
String os = System.getProperty("os.name").toLowerCase();
boolean isWin = os.contains("win");
boolean isMac = os.contains("mac");
if (isWin) {
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",
"C:\\msys64\\ucrt64\\bin\\g++.exe"})
if (new File(p).exists()) return;
// Do NOT fall back to PATH g++ -- it won't have GLFW/GLEW and will
// produce linker errors. Only MSYS2 mingw64 g++ is accepted on Windows.
// Run the wizard unconditionally if MSYS2 g++ not found at known paths.
if (InstallWizard.run(listener)) return;
Object[] opts = { "Install Automatically", "Download MSYS2", "Cancel" };
int ch = javax.swing.JOptionPane.showOptionDialog(null,
new javax.swing.JLabel(
"g++ (C++ compiler) not found.
"
+ "C++ Mode requires MSYS2 with g++, GLFW, and GLEW.
"
+ "Auto-Install (recommended):
"
+ " Click Auto-Install to install everything automatically.
"
+ "Manual:
"
+ " 1. Install MSYS2 from https://www.msys2.org
"
+ " 2. Open MSYS2 MinGW 64-bit terminal
"
+ " 3. Run:
"
+ " pacman -S mingw-w64-x86_64-gcc"
+ " mingw-w64-x86_64-glfw mingw-w64-x86_64-glew
"
+ " 4. Restart Processing"),
"C++ Compiler Not Found",
javax.swing.JOptionPane.YES_NO_CANCEL_OPTION,
javax.swing.JOptionPane.ERROR_MESSAGE,
null, opts, opts[0]);
if (ch == 0) { autoInstallWindows(listener); return; }
else if (ch == 1) {
try { java.awt.Desktop.getDesktop().browse(new java.net.URI("https://www.msys2.org")); }
catch (Exception ignored) {}
}
listener.statusError("g++ not found â install MSYS2 to use C++ Mode");
throw new Exception("g++ not found");
} else {
try { if (Runtime.getRuntime().exec(new String[]{"g++","--version"}).waitFor()==0) return; }
catch (Exception ignored) {}
if (isMac) {
// Try the guided installer first (Xcode Command Line Tools, then
// GLFW/GLEW via Homebrew if present). A cancel throws and stops
// the build; only a genuine failure falls through to the dialog.
if (InstallWizard.run(listener)) return;
showGppDialog(
"g++ not found.
"
+ "Install via Homebrew:
"
+ " brew install gcc glfw glew
"
+ "Click Download Homebrew if not installed.",
"Download Homebrew", "https://brew.sh", listener);
} else {
// Try the guided installer first. A cancel throws and stops the
// build; only a genuine failure falls through to the dialog below.
if (InstallWizard.run(listener)) return;
// Detect package manager and offer auto-install
String[] aptPkgs = {"g++", "libglfw3-dev", "libglew-dev"};
String[] pacmanPkgs = {"gcc", "glfw-x11", "glew"};
String[] dnfPkgs = {"gcc-c++", "glfw-devel", "glew-devel"};
String[] zypperPkgs = {"gcc-c++", "glfw-devel", "glew-devel"};
String[] emergePkgs = {"media-libs/glfw", "media-libs/glew"};
String[] nixPkgs = {"gcc", "glfw", "glew"};
String pm = null;
String[] pmCmd = null;
String pmName = null;
// Detect package manager
java.util.LinkedHashMap managers = new java.util.LinkedHashMap<>();
managers.put("apt-get", new String[]{"apt-get","install","-y","g++","libglfw3-dev","libglew-dev"});
managers.put("apt", new String[]{"apt","install","-y","g++","libglfw3-dev","libglew-dev"});
managers.put("pacman", new String[]{"pacman","-S","--noconfirm","gcc","glfw-x11","glew"});
managers.put("dnf", new String[]{"dnf","install","-y","gcc-c++","glfw-devel","glew-devel"});
managers.put("yum", new String[]{"yum","install","-y","gcc-c++","glfw-devel","glew-devel"});
managers.put("zypper", new String[]{"zypper","install","-y","gcc-c++","glfw-devel","glew-devel"});
managers.put("emerge", new String[]{"emerge","media-libs/glfw","media-libs/glew","sys-devel/gcc"});
managers.put("nix-env", new String[]{"nix-env","-iA","nixpkgs.gcc","nixpkgs.glfw","nixpkgs.glew"});
managers.put("brew", new String[]{"brew","install","gcc","glfw","glew"});
for (java.util.Map.Entry e : managers.entrySet()) {
try {
Process p2 = Runtime.getRuntime().exec(new String[]{"which", e.getKey()});
if (p2.waitFor() == 0) { pm = e.getKey(); pmCmd = e.getValue(); pmName = e.getKey(); break; }
} catch (Exception ignored) {}
}
String installLine = pm != null
? "Detected: " + pm + "
"
+ "Will run:
sudo " + String.join(" ", pmCmd) + ""
: "Could not detect package manager.
Install manually and restart Processing.";
Object[] opts = pm != null
? new Object[]{"Install Automatically", "More Info", "Cancel"}
: new Object[]{"More Info", "Cancel"};
int ch = javax.swing.JOptionPane.showOptionDialog(null,
new javax.swing.JLabel(
"g++ (C++ compiler) not found.
"
+ installLine + "
"
+ "Manual install:
"
+ " apt/apt-get (Ubuntu, Debian, Pop!_OS, Mint):
"
+ " sudo apt install g++ libglfw3-dev libglew-dev
"
+ " pacman (Arch, Manjaro, EndeavourOS):
"
+ " sudo pacman -S gcc glfw-x11 glew
"
+ " dnf/yum (Fedora, RHEL, CentOS):
"
+ " sudo dnf install gcc-c++ glfw-devel glew-devel
"
+ " zypper (openSUSE):
"
+ " sudo zypper install gcc-c++ glfw-devel glew-devel
"
+ " emerge (Gentoo):
"
+ " sudo emerge media-libs/glfw media-libs/glew
"
+ " brew (macOS/Linuxbrew):
"
+ " brew install gcc glfw glew"),
"C++ Compiler Not Found",
javax.swing.JOptionPane.YES_NO_CANCEL_OPTION,
javax.swing.JOptionPane.ERROR_MESSAGE,
null, opts, opts[0]);
if (pm != null && ch == 0) {
autoInstallLinux(pmCmd, listener);
return;
} else if ((pm != null && ch == 1) || (pm == null && ch == 0)) {
try { java.awt.Desktop.getDesktop().browse(new java.net.URI("https://gcc.gnu.org/install/")); }
catch (Exception ignored) {}
}
listener.statusError("g++ not found â install with your package manager");
throw new Exception("g++ not found");
}
}
}
public File compile(RunnerListener listener) throws Exception {
// Check g++ is available before doing anything else
checkGppAvailable(listener);
File sketchSrc = writeSketch(listener);
boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win");
boolean isMac = System.getProperty("os.name").toLowerCase().contains("mac");
String ext = isWindows ? ".exe" : "";
File binary = new File(buildDir, sketch.getName() + ext);
String gpp = findGpp(isWindows);
List cmd = buildCommand(gpp, sketchSrc, binary, isWindows, isMac);
listener.statusNotice("$ " + String.join(" ", cmd));
// Only print the full compile command when debug mode is actually on
// (i.e. the DEBUG file already caused buildCommand() to add
// -DPROCESSING_DEBUG above) -- this is itself a debugging aid, so it
// follows the same toggle as everything else PDEBUG-related, instead
// of always printing regardless of the DEBUG file's contents.
if (cmd.contains("-DPROCESSING_DEBUG")) {
System.err.println("=== FULL COMPILE COMMAND ===");
System.err.println(String.join(" ", cmd));
}
ProcessBuilder pb = new ProcessBuilder(cmd);
pb.redirectErrorStream(true);
pb.directory(buildDir);
// Pass sketch folder so Processing.cpp can find data/ assets
pb.environment().put("PROCESSING_SKETCH_PATH", sketch.getFolder().getAbsolutePath());
Process proc = pb.start();
StringBuilder fullOutput = new StringBuilder();
int firstErrLine = -1;
try (BufferedReader br = new BufferedReader(
new InputStreamReader(proc.getInputStream()))) {
String line;
while ((line = br.readLine()) != null) {
fullOutput.append(line).append("\n");
if (firstErrLine < 0) {
Matcher m = ERR_LINE.matcher(line);
if (m.find()) {
try { firstErrLine = Integer.parseInt(m.group(1)); }
catch (NumberFormatException ignored) {}
}
}
if (line.contains("error:") || line.contains("warning:")) {
for (String w : wordWrap(line, 120)) System.err.println(w);
} else if (!line.isBlank()) {
for (String w : wordWrap(line, 120)) System.out.println(w);
}
}
}
int exitCode = proc.waitFor();
if (exitCode != 0) {
if (firstErrLine > 0 && listener instanceof CppEditor) {
final int el = Math.max(1, firstErrLine - headerLineCount);
javax.swing.SwingUtilities.invokeLater(() ->
((CppEditor) listener).highlightErrorLine(el));
}
listener.statusError("Build failed â see console for errors.");
throw new Exception(fullOutput.toString());
}
// Copy default.ttf to sketch folder so the binary finds it at runtime
File font = new File(runtimeDir, "default.ttf");
if (font.exists()) {
File dest = new File(sketch.getFolder(), "default.ttf");
if (!dest.exists())
java.nio.file.Files.copy(font.toPath(), dest.toPath());
}
listener.statusNotice("Built: " + binary.getName());
// Check for required native libraries on all platforms
checkNativeLibs(listener, binary);
return binary;
}
private boolean commandExists(String cmd) {
try {
Process p = Runtime.getRuntime().exec(new String[]{ cmd, "--version" });
return p.waitFor() == 0;
} catch (Exception e) { return false; }
}
private void checkNativeLibs(RunnerListener listener, File binary) throws Exception {
String os = System.getProperty("os.name").toLowerCase();
boolean win = os.contains("win");
boolean mac = os.contains("mac");
if (win) {
checkWindowsDLLs(listener, binary);
} else if (mac) {
checkMacLibs(listener);
} else {
checkLinuxLibs(listener);
}
}
// âÂÂâ Windows âÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂ
private boolean windowsLibsNowPresent(java.util.List searchDirs, String[] required) {
outer:
for (String dll : required) {
for (String dir : searchDirs)
if (new File(dir, dll).exists()) continue outer;
return false;
}
return true;
}
private void checkWindowsDLLs(RunnerListener listener, File binary) throws Exception {
String[] required = { "glfw3.dll", "glew32.dll" };
java.util.List searchDirs = new java.util.ArrayList<>();
searchDirs.add(binary.getParent());
searchDirs.add("C:\\msys64\\mingw64\\bin");
searchDirs.add("C:\\msys2\\mingw64\\bin");
String path = System.getenv("PATH");
if (path != null) for (String p : path.split(";")) searchDirs.add(p);
java.util.List missing = new java.util.ArrayList<>();
outer:
for (String dll : required) {
for (String dir : searchDirs)
if (new File(dir, dll).exists()) continue outer;
missing.add(dll);
}
if (missing.isEmpty()) return;
// Try the guided installer first.
if (InstallWizard.run(listener)) {
if (windowsLibsNowPresent(searchDirs, required)) return;
}
int choice = javax.swing.JOptionPane.showConfirmDialog(null,
"Missing libraries: " + String.join(", ", missing) + "\n\n" +
"C++ Mode requires GLFW and GLEW to run sketches.\n" +
"Click OK to install them automatically via MSYS2.",
"Missing Libraries", javax.swing.JOptionPane.OK_CANCEL_OPTION,
javax.swing.JOptionPane.WARNING_MESSAGE);
if (choice != javax.swing.JOptionPane.OK_OPTION) return;
// Find MSYS2 pacman
String pacman = null;
for (String p : new String[]{
"C:\\msys64\\usr\\bin\\pacman.exe",
"C:\\msys2\\usr\\bin\\pacman.exe"})
if (new File(p).exists()) { pacman = p; break; }
if (pacman == null) {
try { java.awt.Desktop.getDesktop().browse(new java.net.URI("https://www.msys2.org")); }
catch (Exception ignored) {}
javax.swing.JOptionPane.showMessageDialog(null,
"MSYS2 not found. Please install it from https://www.msys2.org\n\n" +
"Then open MSYS2 MinGW 64-bit and run:\n" +
" pacman -S mingw-w64-x86_64-glfw mingw-w64-x86_64-glew\n\n" +
"Then restart Processing.",
"Install MSYS2", javax.swing.JOptionPane.INFORMATION_MESSAGE);
return;
}
final String finalPacman = pacman;
new Thread(() -> {
try {
listener.statusNotice("Installing GLFW and GLEW via MSYS2...");
ProcessBuilder pb = new ProcessBuilder(finalPacman, "-S", "--noconfirm",
"mingw-w64-x86_64-glfw", "mingw-w64-x86_64-glew");
pb.redirectErrorStream(true);
Process p = pb.start();
try (java.io.BufferedReader br = new java.io.BufferedReader(
new java.io.InputStreamReader(p.getInputStream()))) {
String line; while ((line = br.readLine()) != null) System.out.println(line);
}
if (p.waitFor() == 0) {
// Copy DLLs next to binary
for (String dll : new String[]{ "glfw3.dll","glew32.dll",
"libgcc_s_seh-1.dll","libstdc++-6.dll","libwinpthread-1.dll" }) {
for (String dir : new String[]{
"C:\\msys64\\mingw64\\bin","C:\\msys2\\mingw64\\bin"}) {
File src = new File(dir, dll);
if (src.exists()) {
try { java.nio.file.Files.copy(src.toPath(),
new File(binary.getParent(), dll).toPath(),
java.nio.file.StandardCopyOption.REPLACE_EXISTING); }
catch (Exception ignored) {}
break;
}
}
}
listener.statusNotice("Libraries installed â you can now run your sketch.");
javax.swing.JOptionPane.showMessageDialog(null,
"GLFW and GLEW installed successfully!\nYou can now run your sketch.",
"Done", javax.swing.JOptionPane.INFORMATION_MESSAGE);
} else {
listener.statusError("Installation failed â try manually in MSYS2 MinGW 64-bit terminal.");
}
} catch (Exception e) { listener.statusError("Install error: " + e.getMessage()); }
}).start();
}
// âÂÂâ macOS âÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂ
private void checkMacLibs(RunnerListener listener) throws Exception {
// Check for glfw and glew via pkg-config or known Homebrew paths
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");
if (glfwOk && glewOk) return;
java.util.List missing = new java.util.ArrayList<>();
if (!glfwOk) missing.add("glfw");
if (!glewOk) missing.add("glew");
// Try the guided installer first.
if (InstallWizard.run(listener)) {
boolean glfwNow = new File("/opt/homebrew/lib/libglfw.dylib").exists()
|| new File("/usr/local/lib/libglfw.dylib").exists()
|| commandExists("pkg-config glfw3");
boolean glewNow = new File("/opt/homebrew/lib/libGLEW.dylib").exists()
|| new File("/usr/local/lib/libGLEW.dylib").exists()
|| commandExists("pkg-config glew");
if (glfwNow && glewNow) return;
}
int choice = javax.swing.JOptionPane.showConfirmDialog(null,
"Missing libraries: " + String.join(", ", missing) + "\n\n" +
"C++ Mode requires GLFW and GLEW to run sketches.\n" +
"Click OK to install them via Homebrew.",
"Missing Libraries", javax.swing.JOptionPane.OK_CANCEL_OPTION,
javax.swing.JOptionPane.WARNING_MESSAGE);
if (choice != javax.swing.JOptionPane.OK_OPTION) return;
// Check if brew is available
if (!commandExists("brew")) {
try { java.awt.Desktop.getDesktop().browse(new java.net.URI("https://brew.sh")); }
catch (Exception ignored) {}
javax.swing.JOptionPane.showMessageDialog(null,
"Homebrew not found. Please install it from https://brew.sh\n\n" +
"Then run:\n brew install glfw glew\n\nThen restart Processing.",
"Install Homebrew", javax.swing.JOptionPane.INFORMATION_MESSAGE);
return;
}
new Thread(() -> {
try {
listener.statusNotice("Installing GLFW and GLEW via Homebrew...");
ProcessBuilder pb = new ProcessBuilder("brew", "install", "glfw", "glew");
pb.redirectErrorStream(true);
Process p = pb.start();
try (java.io.BufferedReader br = new java.io.BufferedReader(
new java.io.InputStreamReader(p.getInputStream()))) {
String line; while ((line = br.readLine()) != null) System.out.println(line);
}
if (p.waitFor() == 0) {
listener.statusNotice("Libraries installed â you can now run your sketch.");
javax.swing.JOptionPane.showMessageDialog(null,
"GLFW and GLEW installed successfully!\nYou can now run your sketch.",
"Done", javax.swing.JOptionPane.INFORMATION_MESSAGE);
} else {
listener.statusError("Installation failed â try: brew install glfw glew");
}
} catch (Exception e) { listener.statusError("Install error: " + e.getMessage()); }
}).start();
}
// âÂÂâ Linux âÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂ
private void checkLinuxLibs(RunnerListener listener) throws Exception {
boolean glfwOk = new File("/usr/lib/libglfw.so").exists()
|| new File("/usr/lib/x86_64-linux-gnu/libglfw.so").exists()
|| new File("/usr/lib/x86_64-linux-gnu/libglfw.so.3").exists()
|| commandExists("pkg-config glfw3");
boolean glewOk = new File("/usr/lib/libGLEW.so").exists()
|| new File("/usr/lib/x86_64-linux-gnu/libGLEW.so").exists()
|| commandExists("pkg-config glew");
if (glfwOk && glewOk) return;
// Try the guided installer first.
if (InstallWizard.run(listener)) {
boolean glfwNow = new File("/usr/lib/libglfw.so").exists()
|| new File("/usr/lib/x86_64-linux-gnu/libglfw.so").exists()
|| new File("/usr/lib/x86_64-linux-gnu/libglfw.so.3").exists()
|| commandExists("pkg-config glfw3");
boolean glewNow = new File("/usr/lib/libGLEW.so").exists()
|| new File("/usr/lib/x86_64-linux-gnu/libGLEW.so").exists()
|| commandExists("pkg-config glew");
if (glfwNow && glewNow) return;
}
java.util.List missing = new java.util.ArrayList<>();
if (!glfwOk) missing.add("libglfw3-dev");
if (!glewOk) missing.add("libglew-dev");
// Detect package manager
String installCmd = detectLinuxInstallCmd(missing);
int choice = javax.swing.JOptionPane.showConfirmDialog(null,
"Missing libraries: " + String.join(", ", missing) + "\n\n" +
"C++ Mode requires GLFW and GLEW to run sketches.\n" +
"Click OK to install them automatically.",
"Missing Libraries", javax.swing.JOptionPane.OK_CANCEL_OPTION,
javax.swing.JOptionPane.WARNING_MESSAGE);
if (choice != javax.swing.JOptionPane.OK_OPTION) return;
if (installCmd == null) {
javax.swing.JOptionPane.showMessageDialog(null,
"Unknown Linux distribution.
"
+ "Please install g++, GLFW, and GLEW manually
"
+ "using your distro's package manager,
"
+ "then restart Processing.",
"Missing Libraries", javax.swing.JOptionPane.WARNING_MESSAGE);
return;
}
final String cmd = installCmd;
new Thread(() -> {
try {
listener.statusNotice("Installing libraries...");
// Write install script to temp file and run it in a terminal
java.io.File tmp = java.io.File.createTempFile("cpp_install_", ".sh");
tmp.deleteOnExit();
try (java.io.PrintWriter pw = new java.io.PrintWriter(tmp)) {
pw.println("#!/bin/bash");
pw.println("echo '==============================='");
pw.println("echo ' C++ Mode Library Installer '");
pw.println("echo '==============================='");
pw.println("echo ''");
pw.println("show_progress() {");
pw.println(" local pid=$1 msg=$2 i=0");
pw.println(" local spin='|/-\\\\'");
pw.println(" while kill -0 $pid 2>/dev/null; do");
pw.println(" i=$(( (i+1) % 4 ))");
pw.println(" printf '\\r [%s] %s' \"${spin:$i:1}\" \"$msg\"");
pw.println(" sleep 0.15");
pw.println(" done");
pw.println(" printf '\\r [OK] %s\\n' \"$msg\"");
pw.println("}");
pw.println("echo 'Installing g++, GLFW, GLEW...'"); pw.println("echo 'Installing g++, GLFW, GLEW...'");
pw.println("echo ''");
pw.println(cmd + " &");
pw.println("PKG_PID=$!");
pw.println("spinner $PKG_PID 'Installing packages...'");
pw.println("wait $PKG_PID");
pw.println("EXIT=$?");
pw.println("echo ''");
pw.println("if [ $EXIT -eq 0 ]; then");
pw.println(" echo '==============================='");
pw.println(" echo ' Installation complete!'");
pw.println(" echo ' Please restart Processing.'");
pw.println(" echo '==============================='");
pw.println("else");
pw.println(" echo 'Installation failed. Try running manually:'");
pw.println(" echo '" + cmd + "'");
pw.println("fi");
pw.println("read -p 'Press Enter to close...'");
}
tmp.setExecutable(true);
// Try to open in a terminal emulator
String[] terminals = {
"x-terminal-emulator", "gnome-terminal", "konsole",
"xfce4-terminal", "xterm", "alacritty", "kitty"
};
boolean launched = false;
for (String term : terminals) {
if (!cmdExists(term)) continue;
String[] termCmd;
if (term.equals("gnome-terminal"))
termCmd = new String[]{ term, "--", "bash", tmp.getAbsolutePath() };
else if (term.equals("alacritty") || term.equals("kitty"))
termCmd = new String[]{ term, "-e", "bash", tmp.getAbsolutePath() };
else
termCmd = new String[]{ term, "-e", "bash " + tmp.getAbsolutePath() };
try {
new ProcessBuilder(termCmd).start();
launched = true;
break;
} catch (Exception ignored) {}
}
if (!launched) {
// Fallback: run directly
ProcessBuilder pb = new ProcessBuilder("bash", tmp.getAbsolutePath());
pb.redirectErrorStream(true);
Process p = pb.start();
try (java.io.BufferedReader br = new java.io.BufferedReader(
new java.io.InputStreamReader(p.getInputStream()))) {
String line; while ((line = br.readLine()) != null) System.out.println(line);
}
p.waitFor();
}
listener.statusNotice("Installer launched â restart Processing when done.");
javax.swing.JOptionPane.showMessageDialog(null,
"Installer launched!
"
+ "When it says Installation complete!:
"
+ " 1. Close the terminal
"
+ " 2. Restart Processing",
"Installing...", javax.swing.JOptionPane.INFORMATION_MESSAGE);
} catch (Exception e) { listener.statusError("Install error: " + e.getMessage()); }
}).start();
}
private boolean cmdExists(String cmd) {
try {
String os = System.getProperty("os.name","").toLowerCase();
ProcessBuilder pb;
if (os.contains("win")) {
// On Windows use 'where' instead of 'which'
pb = new ProcessBuilder("where", cmd);
} else {
pb = new ProcessBuilder("which", cmd);
}
pb.redirectErrorStream(true);
return pb.start().waitFor() == 0;
} catch (Exception e) { return false; }
}
private boolean compilerExists(ExportTarget t) {
String os = System.getProperty("os.name","").toLowerCase();
boolean isWin = os.contains("win");
// macOS cross-compile needs osxcross - not bundled
if (t == ExportTarget.MACOS_X64 || t == ExportTarget.MACOS_ARM64)
return cmdExists(t.compiler);
// Linux targets: need Linux/Mac to cross-compile
if (!t.isWindows) {
if (t == ExportTarget.MACOS_X64 || t == ExportTarget.MACOS_ARM64)
return cmdExists(t.compiler);
if (isWin) return false; // not available on Windows - need Linux machine
if (muslToolchainInstalled(t)) return true;
return cmdExists(t.compiler);
}
// Windows target
if (isWin) {
String[] msysPaths = {
"C:\\msys64\\mingw64\\bin\\g++.exe",
"C:\\msys2\\mingw64\\bin\\g++.exe",
System.getProperty("user.home") + "\\msys64\\mingw64\\bin\\g++.exe"
};
for (String p : msysPaths)
if (new java.io.File(p).exists()) return true;
return cmdExists("g++");
}
return cmdExists(t.compiler);
}
private String detectLinuxInstallCmd(java.util.List aptPkgs) {
if (cmdExists("apt-get"))
return "pkexec apt-get install -y libglfw3-dev libglew-dev g++";
if (cmdExists("pacman"))
return "pkexec bash -c 'pacman -S --noconfirm --overwrite=* gcc glfw-x11 glew"
+ " || pacman -S --noconfirm --overwrite=* gcc glfw glew'";
if (cmdExists("dnf"))
return "pkexec dnf install -y gcc-c++ glfw-devel glew-devel";
if (cmdExists("zypper"))
return "pkexec zypper install -y gcc-c++ libglfw3 libglfw-devel glew-devel";
if (cmdExists("emerge"))
return "pkexec emerge --ask=n media-libs/glfw media-libs/glew sys-devel/gcc";
if (cmdExists("xbps-install"))
return "pkexec xbps-install -y gcc glfw-devel glew";
if (cmdExists("apk"))
return "pkexec apk add --no-cache gcc g++ glfw-dev glew-dev";
return null;
}
// Lifecycle/event method names, shared between the AST pipeline's
// override-marking and dependency-exclusion rules. Same list as the
// original's local "lifecycleMethods" array, just hoisted to a
// constant since both LifecycleRewriter and DependencyHoister need it.
private static final java.util.Set LIFECYCLE_METHOD_NAMES = java.util.Set.of(
"setup", "draw", "settings",
"mousePressed", "mouseReleased", "mouseClicked",
"mouseMoved", "mouseDragged", "mouseWheel",
"keyPressed", "keyReleased", "keyTyped",
"windowMoved", "windowResized"
);
/**
* Replaces the original writeSketch()'s regex/character-walking
* hoisting and rewriting passes (hoistClassesOnly, removeHoistedClasses,
* classifyTopLevelDecls-based array/variable/function hoisting,
* rewriteAsSketchMethods, the inline enumScope/constexprScope
* extraction, and the inline _PSketch-injection regex chain) with the
* real AST pipeline built and tested in tools/cpp-parser/ (see
* DECISION_two_parser_implementations.md for the full history,
* including two real bugs found and fixed via PipelineCompositionTest
* before this was wired in here).
*
* Every literal scaffolding string this method emits (the #include
* lines, the using-declarations, the _PSketch struct body, the
* free-function forwarding, and main()) is UNCHANGED from the
* original -- none of that depends on parsing user code at all, so
* none of it needed to change. Only the user-code transformation
* steps (hoisting, lifecycle override-marking, pointer-defaulting)
* were replaced.
*
* NOT YET PORTED to this pipeline, carried over as known gaps from
* DECISION_two_parser_implementations.md: constexpr/static_assert/
* type-alias "using" extraction (zero corpus evidence for any of
* these -- see EnumScopeExtractor's javadoc), and the most-vexing-
* parse direct-init rewrite for any case OTHER than the one this
* pipeline's CodeGen already emits as brace-init unconditionally
* (CodeGen renders ALL direct-init as braces now, which is a
* strictly safer superset of the original's behavior, not a gap).
*/
/**
* Wraps Parser.parse(code) with the same error-reporting convention
* already used by checkForUnsupportedJavaArraySyntax (detailed message
* to System.err, short message to listener.statusError(), then a
* RuntimeException) instead of letting a ParseException propagate as
* a raw, unhandled exception all the way up through writeSketch() ->
* compile() -> CppMode.handleLaunch() -> whatever the outer Processing
* IDE framework happens to do with an arbitrary uncaught exception
* from a Mode plugin.
*
* Found this gap while investigating a deliberately-left-unfixed
* parser limitation (non-type template arguments, e.g.
* "std::array") and confirming it fails GRACEFULLY (a clean
* ParseException with an accurate line/column, not a crash or a
* silent misparse) rather than just assuming so -- tracing where that
* exception actually goes surfaced that NOTHING in this Mode's own
* code catches it at all, at any layer, before this fix. Whether
* processing4's own core Editor/Sketch-running machinery already
* catches an arbitrary uncaught exception from a Mode's
* handleLaunch() with a reasonable user-facing error isn't something
* this project's available source could confirm either way -- this
* fix makes the Mode's OWN behavior strictly better regardless of
* what that outer layer does, by ensuring the sketch author sees a
* clean, accurate "your code has a syntax error at line N, column M"
* message via the IDE's own status bar specifically, the same way
* they already do for E0004 (Java array syntax) and would for any
* other Mode-level validation error.
*
* Throws AlreadyReportedException (not a plain RuntimeException) so
* the writeSketch() safety-net wrapper added alongside this (see its
* own javadoc) can tell "this was already cleanly reported to the
* user via listener.statusError()" apart from "this is a genuinely
* unexpected crash the user hasn't seen a message for yet" -- without
* relying on fragile message-string matching to make that distinction.
*/
private static final class AlreadyReportedException extends RuntimeException {
AlreadyReportedException(String message) { super(message); }
}
private static String stripRawStringLiterals(String code) {
StringBuilder out = new StringBuilder();
int i = 0;
while (i < code.length()) {
if (i + 1 < code.length() && code.charAt(i) == 'R' && code.charAt(i+1) == '"') {
int j = i + 2;
StringBuilder delim = new StringBuilder();
while (j < code.length() && code.charAt(j) != '(' && code.charAt(j) != '"') {
delim.append(code.charAt(j++));
}
if (j < code.length() && code.charAt(j) == '(') {
String closer = ")" + delim.toString() + "\"";
int end = code.indexOf(closer, j + 1);
if (end >= 0) {
out.append("\"\"");
i = end + closer.length();
continue;
}
}
}
out.append(code.charAt(i++));
}
return out.toString();
}
private CompilationUnit parseOrReportError(String code, RunnerListener listener) {
try {
return Parser.parse(code);
} catch (ParseException e) {
System.err.println(e.getMessage());
listener.statusError("Syntax error: " + e.getMessage());
throw new AlreadyReportedException("Syntax error: " + e.getMessage());
}
}
/**
* Safety-net wrapper around writeSketchImpl(). The parse step itself
* is already guarded (see parseOrReportError() and
* checkForUnsupportedJavaArraySyntax(), both throwing
* AlreadyReportedException after cleanly reporting via
* listener.statusError()) -- but writeSketchImpl() also makes roughly
* twenty further calls into the AST pipeline (ClassHoister,
* PSketchInjector, ArrayHoister, DependencyHoister, ForwardDeclGenerator,
* LifecycleRewriter, EnumScopeExtractor, CodeGen), none of which were
* individually guarded. RealHeaderStressTest/RealCorpusStressTest
* confirmed zero crashes across the full 131-file real example
* corpus, both against a hand-built stub and the real engine headers
* -- but "zero crashes on every sketch tested so far" isn't the same
* guarantee as "zero crashes on any sketch anyone ever writes," and an
* unexpected exception from deep in the pipeline (a NullPointerException,
* ClassCastException, or anything else not yet seen) would otherwise
* propagate exactly as raw and unhandled as the ParseException gap
* this same investigation found and fixed.
*
* Catches any RuntimeException that ISN'T already an
* AlreadyReportedException (avoiding a double report for an error the
* person has already seen via statusError()) and reports it the same
* way, with a generic but still useful message plus the real
* exception detail in the console -- strictly better than an
* unhandled crash, even though "an internal error occurred" is
* necessarily less specific than a real syntax-error message can be.
*/
private File writeSketch(RunnerListener listener) throws IOException {
try {
return writeSketchImpl(listener);
} catch (AlreadyReportedException e) {
throw e; // already reported via listener.statusError() at the point it was thrown -- just propagate
} catch (RuntimeException e) {
System.err.println("Internal CppMode error while preparing the sketch for compilation:");
e.printStackTrace();
listener.statusError("Internal error preparing sketch -- see console for details: " + e.getMessage());
throw new AlreadyReportedException("Internal error preparing sketch: " + e.getMessage());
}
}
private File writeSketchImpl(RunnerListener listener) throws IOException {
StringBuilder prefix = new StringBuilder();
StringBuilder suffix = new StringBuilder();
for (int i = 0; i < sketch.getCodeCount(); i++) {
String prog = sketch.getCode(i).getProgram();
boolean hasLifecycle = prog.contains("void setup(") || prog.contains("void draw(");
// Non-lifecycle tabs (class definitions etc.) go before lifecycle tabs,
// preserving their original tab order. Previously used raw.insert(0, prog)
// which reversed non-lifecycle tab order -- so Flock.pde ended up before
// Boid.pde even when Flock depends on Boid (Processing sorts tabs
// alphabetically, so B < F means Boid is processed first, then inserted
// at position 0 AFTER Flock, putting Flock first -- backwards).
if (!hasLifecycle) prefix.append(prog).append("\n");
else suffix.append(prog).append("\n");
}
StringBuilder raw = new StringBuilder();
raw.append(prefix).append(suffix);
String code = sanitize(raw.toString());
code = removeUserIncludes(code);
// Restore noexcept on coroutine promise methods (parser drops noexcept specifier)
code = code.replaceAll("(\\bfinal_suspend\\s*\\([^)]*\\))(\\s*\\{)", "$1 noexcept$2");
code = code.replaceAll("(\\binitial_suspend\\s*\\([^)]*\\))(\\s*\\{)", "$1 noexcept$2");
// Remove no-op translate(0, 0) and translate(0, 0, 0) calls (Problem 7.2)
code = code.replaceAll("\\btranslate\\s*\\(\\s*0+\\.?0*f?\\s*,\\s*0+\\.?0*f?\\s*\\)\\s*;", "");
code = code.replaceAll("\\btranslate\\s*\\(\\s*0+\\.?0*f?\\s*,\\s*0+\\.?0*f?\\s*,\\s*0+\\.?0*f?\\s*\\)\\s*;", "");
warnReservedNames(code, listener);
checkForUnsupportedJavaArraySyntax(code, listener);
checkForArrayListGetValueCopy(code, listener);
checkForArrayListGetDotAccess(code, listener);
checkForJavaStaticCallSyntax(code, listener);
checkForProcessingNameCollisions(code, listener);
code = stripRawStringLiterals(code);
code = code.replaceAll("(?<=[0-9a-fA-FxXbB])'(?=[0-9a-fA-F])", "");
code = javaToC(code);
code = stripNamespaceProcessing(code);
code = preprocessMacros(code);
boolean hasSetup = code.contains("void setup(");
boolean hasDraw = code.contains("void draw(");
StringBuilder out = new StringBuilder();
// preNs collects content that goes BEFORE namespace Processing --
// specifically user #include directives which must be in global scope
// (GCC 16+ headers use unqualified names that don't resolve in ::Processing)
StringBuilder preNs = new StringBuilder();
out.append("#include \"Processing.h\"\n");
out.append("using std::vector; using std::string; using std::wstring;\n");
out.append("using std::pair; using std::make_pair; using std::tuple;\n");
out.append("using std::deque; using std::list; using std::stack; using std::queue;\n");
out.append("using std::unordered_map; using std::unordered_set;\n");
out.append("using std::sort; using std::shuffle; using std::reverse;\n");
out.append("using std::unique_ptr; using std::shared_ptr;\n");
out.append("using std::make_unique; using std::make_shared;\n");
out.append("using std::to_string; using std::stoi; using std::stof; using std::stod;\n");
out.append("using std::cout; using std::cerr; using std::endl;\n");
out.append("using std::ifstream; using std::ofstream; using std::stringstream;\n");
out.append("using std::move; using std::forward; using std::swap;\n");
out.append("using std::begin; using std::end;\n");
out.append("using std::accumulate; using std::transform; using std::find;\n");
out.append("using std::array; using std::span; using std::optional; using std::variant;\n");
out.append("using std::function;\n");
out.append("using std::map; using std::set;\n");
out.append("using std::initializer_list; using std::enable_if; using std::enable_if_t;\n");
out.append("using std::is_integral_v; using std::is_floating_point_v; using std::is_arithmetic_v;\n");
out.append("using std::is_same_v; using std::is_base_of_v; using std::is_convertible_v;\n");
out.append("using std::decay_t; using std::remove_reference_t; using std::common_type_t;\n");
out.append("using std::declval; using std::void_t;\n");
out.append("using std::index_sequence; using std::make_index_sequence;\n");
out.append("using std::tuple_size; using std::tuple_element; using std::get;\n");
out.append("using std::make_tuple; using std::tie; using std::apply;\n");
out.append("using std::runtime_error; using std::logic_error; using std::exception;\n");
out.append("using std::numeric_limits;\n");
out.append(preNs); // user #include directives hoisted to global scope
out.append("using namespace std;\n");
out.append("namespace Processing {\n");
out.append("using namespace std;\n");
out.append("#include \"Processing_api.h\"\n");
out.append("struct _PSketch {\n");
out.append(" struct _W { operator int() const { return ::Processing::PApplet::g_papplet ? ::Processing::PApplet::g_papplet->logicalW : 0; } } width;\n");
out.append(" struct _H { operator int() const { return ::Processing::PApplet::g_papplet ? ::Processing::PApplet::g_papplet->logicalH : 0; } } height;\n");
out.append(" struct _MX { operator float() const { return ::Processing::PApplet::g_papplet ? ::Processing::PApplet::g_papplet->mouseX : 0.f; } } mouseX;\n");
out.append(" struct _MY { operator float() const { return ::Processing::PApplet::g_papplet ? ::Processing::PApplet::g_papplet->mouseY : 0.f; } } mouseY;\n");
out.append(" struct _PMX { operator float() const { return ::Processing::PApplet::g_papplet ? ::Processing::PApplet::g_papplet->pmouseX : 0.f; } } pmouseX;\n");
out.append(" struct _PMY { operator float() const { return ::Processing::PApplet::g_papplet ? ::Processing::PApplet::g_papplet->pmouseY : 0.f; } } pmouseY;\n");
out.append(" struct _FC { operator int() const { return ::Processing::PApplet::g_papplet ? ::Processing::PApplet::g_papplet->frameCount : 0; } } frameCount;\n");
out.append(" struct _MP { operator bool() const { return ::Processing::PApplet::g_papplet ? ::Processing::PApplet::g_papplet->_mousePressed : false; } } _mousePressed;\n");
out.append(" struct _KP { operator bool() const { return ::Processing::PApplet::g_papplet ? ::Processing::PApplet::g_papplet->_keyPressed : false; } } _keyPressed;\n");
out.append(" struct _K { operator char() const { return ::Processing::PApplet::g_papplet ? ::Processing::PApplet::g_papplet->key : 0; } } key;\n");
out.append(" struct _KC { operator int() const { return ::Processing::PApplet::g_papplet ? ::Processing::PApplet::g_papplet->keyCode : 0; } } keyCode;\n");
out.append(" struct _MB { operator int() const { return ::Processing::PApplet::g_papplet ? ::Processing::PApplet::g_papplet->mouseButton : 0; } } mouseButton;\n");
out.append(" struct _FR { operator float() const { return ::Processing::PApplet::g_papplet ? ::Processing::PApplet::g_papplet->_frameRate : 0.f; } } _frameRate;\n");
out.append(" struct _MDX { operator float() const { return ::Processing::PApplet::g_papplet ? ::Processing::PApplet::g_papplet->mouseDX : 0.f; } } mouseDX;\n");
out.append(" struct _MDY { operator float() const { return ::Processing::PApplet::g_papplet ? ::Processing::PApplet::g_papplet->mouseDY : 0.f; } } mouseDY;\n");
out.append(" bool* keysDown() const { return ::Processing::PApplet::g_papplet ? ::Processing::PApplet::g_papplet->keysDown : nullptr; }\n");
out.append(" bool* mouseDown() const { return ::Processing::PApplet::g_papplet ? ::Processing::PApplet::g_papplet->mouseDown : nullptr; }\n");
out.append("};\n");
out.append("\n");
out.append("#line 1 \"sketch.pde\"\n");
headerLineCount = 0;
if (!hasSetup && !hasDraw) {
// âÂÂâ Static sketch (no setup/draw) âÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂ
// Parse the whole thing as a CompilationUnit; the parser itself
// already produces a TopLevelStatement wrapper for bare statements
// at file scope (see Parser/TopLevelStatement's design notes --
// this is exactly the construct this static-mode branch needs).
CompilationUnit cu = parseOrReportError(code, listener);
ClassHoister.Result classResult = ClassHoister.hoist(cu.items());
List injected = PSketchInjector.injectAll(classResult.hoistedClasses);
List finalClasses = injected.stream().map(PSketchInjector.Result::typeDef).toList();
// Preprocessor directives, namespace blocks, and using-namespace
// declarations can never be valid INSIDE a function body or struct --
// #include/namespace/using-namespace are only legal at true file
// scope. The original per-line text scan never had this problem
// (it only ever recognized "size("/"fullScreen(" as special, and
// simply emitted every other line as-is inside setup(), silently
// producing invalid C++ for any sketch with one of these at top
// level -- a real, previously-undetected bug, confirmed by a
// PipelineCompositionTest run against a real fixture with top-level
// "#include "/"#include " lines, which produced
// structurally broken output: the includes landed inside a function
// body). Fixed by emitting these BEFORE the Sketch struct, exactly
// where they'd need to be for valid C++, instead of wherever they
// happened to appear relative to other top-level code.
StringBuilder fileScopeOnly = new StringBuilder();
StringBuilder settings = new StringBuilder();
StringBuilder body = new StringBuilder();
for (TopLevelItem item : classResult.rest) {
if (item instanceof PreprocessorLine pl && pl.rawText().startsWith("#include")) {
preNs.append(CodeGen.generateNode(item, 0)); // hoist before namespace
continue;
}
if (item instanceof PreprocessorLine || item instanceof NamespaceDecl || item instanceof UsingNamespaceDecl) {
fileScopeOnly.append(CodeGen.generateNode(item, 0));
continue;
}
// Template declarations, free functions, and ALL variables must be at
// file scope in static sketches -- main() cannot access Sketch members.
boolean isAnyFn = item instanceof FunctionDecl;
boolean isAnyVar = item instanceof VariableDecl;
boolean isTemplateTd = item instanceof TypeDef td
&& (!td.templateParams().isEmpty() || td.name().contains("<"));
if (isAnyFn || isAnyVar || isTemplateTd) {
fileScopeOnly.append(CodeGen.generateNode(item, 0));
continue;
}
String rendered = CodeGen.generateNode(item, 2);
String trimmed = rendered.strip();
if (trimmed.startsWith("size(") || trimmed.startsWith("fullScreen(")) {
settings.append(rendered);
} else {
body.append(rendered);
}
}
// Emit template/class definitions BEFORE file-scope variables and using aliases.
for (TypeDef td : finalClasses) {
if (RESERVED_TYPES.contains(td.name())) continue; // engine already defines this
out.append(CodeGen.generateNode(td, 0));
}
out.append(fileScopeOnly);
out.append("struct Sketch : public PApplet {\n");
out.append(" void setup() override {\n");
out.append(settings);
out.append(" for (int _s=0; _s<8; _s++) { delay(1); }\n");
out.append(" noLoop();\n");
out.append(body);
out.append(" }\n");
out.append(" void draw() override {}\n");
out.append("};\n");
} else {
// âÂÂâ Normal sketch (has setup/draw) âÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂ
CompilationUnit cu = parseOrReportError(code, listener);
// Step 1: enum extraction (before anything else touches the list) --
// see EnumScopeExtractor's javadoc for the known constexpr/
// static_assert/using-alias gap, carried over unchanged.
EnumScopeExtractor.Result enumResult = EnumScopeExtractor.extract(cu.items());
// Step 2: lifecycle rewriting (override + nullptr defaulting).
List afterLifecycle = LifecycleRewriter.rewrite(enumResult.rest, LIFECYCLE_METHOD_NAMES);
// Step 3: class hoisting.
ClassHoister.Result classResult = ClassHoister.hoist(afterLifecycle);
// Step 4: _PSketch injection.
List injectedClasses = PSketchInjector.injectAll(classResult.hoistedClasses);
List finalClasses = injectedClasses.stream().map(PSketchInjector.Result::typeDef).toList();
// Step 5: array hoisting.
ArrayHoister.Result arrayResult = ArrayHoister.hoist(classResult.rest);
// Step 6: dependency hoisting (functions + plain variables that
// hoisted classes reference).
DependencyHoister.Result depResult = DependencyHoister.hoist(arrayResult.rest, finalClasses, LIFECYCLE_METHOD_NAMES);
// Step 7: forward-declare hoisted functions.
List forwardDecls = ForwardDeclGenerator.generate(depResult.hoistedFunctions);
// Preprocessor directives, namespace blocks, and using-namespace
// declarations can never be valid INSIDE a struct body --
// #include/namespace/using-namespace are only legal at true file
// scope. Found as a real bug via PipelineCompositionTest against a
// fixture with top-level "#include "/"#include
// " lines, which an earlier version of this method placed
// inside struct Sketch, producing invalid C++. Extracted here and
// emitted at the very FRONT of the remaining output (before even
// forward decls), since a forward-declared function's signature
// could in principle reference something from one of these
// includes too -- "before the Sketch struct" alone isn't a strong
// enough guarantee; "before everything else generated" is.
// Hoist "auto" variables to namespace scope -- auto on non-static members is invalid.
List autoHoisted = new ArrayList<>();
List filteredRest = new ArrayList<>();
for (TopLevelItem item : depResult.rest) {
if (item instanceof VariableDecl vd
&& vd.type() instanceof NamedType nt
&& (nt.baseName().equals("auto") || nt.baseName().startsWith("std::function"))) {
autoHoisted.add(item);
} else if (item instanceof FunctionDecl fd
&& fd.name().startsWith("operator")
&& fd.params().size() >= 2) {
// Free binary operator: must be at namespace scope, not struct member
autoHoisted.add(item);
} else if (item instanceof FunctionDecl fdce && fdce.isConstexpr()) {
// constexpr functions must be at namespace scope for static_assert to use them
autoHoisted.add(item);
} else if (item instanceof VariableDecl vdq && vdq.name().contains("::")) {
// Out-of-class static member definition: "int Agent::totalAgents = 0"
// must be at namespace scope, never inside struct Sketch
autoHoisted.add(item);
} else if (item instanceof FunctionDecl fdoc && fdoc.name().contains("::")) {
// Out-of-class function definition: "Generator17 GeneratorPromise17::get_return_object()"
// must be at namespace scope, never inside struct Sketch
autoHoisted.add(item);
} else {
filteredRest.add(item);
}
}
// Replace depResult.rest with filtered version
List effectiveRest = filteredRest;
List sketchMembers = new ArrayList<>();
List deferredAliases = new ArrayList<>();
for (TopLevelItem item : effectiveRest) {
if (item instanceof PreprocessorLine pl && pl.rawText().startsWith("#include")) {
preNs.append(CodeGen.generateNode(item, 0)); // hoist before namespace
} else if (item instanceof PreprocessorLine pl2) {
// Defer "using X = ..." type aliases (including template aliases) until after struct definitions
String rt = pl2.rawText().trim();
// Defer "using X = ..." type aliases and deduction guides after struct definitions
boolean isTypeAlias = (rt.startsWith("using ") || rt.contains(" using ")) && !rt.contains("using namespace") && rt.contains("=");
// Deduction guide: "Name(params) -> Name;" -- NOT concepts with requires { } ->
// Distinguish by checking -> appears before any { (deduction guides have no braces)
boolean isDeductionGuide = rt.contains("->") && rt.contains("(") && !rt.startsWith("#")
&& !rt.contains("concept") && !rt.contains("requires") && (rt.indexOf("{") < 0 || rt.indexOf("->") < rt.indexOf("{"));
if (isTypeAlias || isDeductionGuide) {
deferredAliases.add(item);
} else if (rt.startsWith("template ") && !rt.startsWith("template<") && !rt.startsWith("template <")) {
// Explicit template instantiation: must be outside namespace Processing
preNs.append(CodeGen.generateNode(item, 0));
} else {
out.append(CodeGen.generateNode(item, 0));
}
} else if (item instanceof NamespaceDecl nd) {
// User namespace: emit inside namespace Processing.
// CodeGen injects "using namespace ::std" inside namespace bodies
// so std:: resolves to global ::std not Processing::std.
// std specializations (namespace std { ... }) go to preNs (global scope).
if (nd.name().equals("std")) {
preNs.append(CodeGen.generateNode(nd, 0));
} else {
out.append(CodeGen.generateNode(nd, 0));
}
} else if (item instanceof UsingNamespaceDecl) {
out.append(CodeGen.generateNode(item, 0));
} else {
sketchMembers.add(item);
}
}
// Step 8: emit in the same order the original's StringBuilder
// concatenation used: forward decls, enums, hoisted vars, hoisted
// arrays, hoisted classes, hoisted functions, then struct Sketch
// wrapping everything left in sketchMembers (depResult.rest minus
// the file-scope-only items already emitted above).
for (var e : enumResult.enums) out.append(CodeGen.generateNode(e, 0));
for (FunctionDecl fd : forwardDecls) out.append(CodeGen.generateNode(fd, 0));
for (var v : arrayResult.hoistedSizingConstants) out.append(CodeGen.generateNode(v, 0));
// Emit hoisted dependency variables BEFORE class definitions so
// hoisted classes can reference them (e.g. FloatList readings used by SensorBank).
java.util.Set autoHoistedNames2 = new java.util.HashSet<>();
for (TopLevelItem av : autoHoisted) if (av instanceof VariableDecl avd2) autoHoistedNames2.add(avd2.name());
for (var v : depResult.hoistedVariables) {
if (!autoHoistedNames2.contains(v.name())) out.append(CodeGen.generateNode(v, 0));
}
// Forward-declare every hoisted class before any definitions -- handles
// cross-references between classes (e.g. Liquid::contains(Mover*) when
// Liquid is emitted before Mover). Template classes can't be forward-declared
// this way, but user template classes are rare enough that this is fine.
for (TypeDef td : finalClasses) {
if (RESERVED_TYPES.contains(td.name())) continue; // engine already defines this
if (td.templateParams().isEmpty() && !td.name().contains("<")) {
out.append(td.kind()).append(' ').append(td.name()).append(";\n");
}
}
for (TypeDef td : finalClasses) {
if (RESERVED_TYPES.contains(td.name())) continue; // engine already defines this
out.append(CodeGen.generateNode(td, 0));
}
// Emit deferred "using X = ..." type aliases after struct definitions
for (TopLevelItem alias : deferredAliases) out.append(CodeGen.generateNode(alias, 0));
// Emit auto-hoisted variables at namespace scope
for (TopLevelItem av : autoHoisted) out.append(CodeGen.generateNode(av, 0));
// hoistedVariables already emitted before class definitions above
for (var v : arrayResult.hoistedArrays) out.append(CodeGen.generateNode(v, 0));
for (FunctionDecl fd : depResult.hoistedFunctions) out.append(CodeGen.generateNode(fd, 0));
// constexprScope (the NOT-PORTED half -- see EnumScopeExtractor's
// javadoc) intentionally has no corresponding emission here, since
// nothing in the pipeline currently extracts it; any constexpr/
// static_assert/using-alias declaration a sketch might contain
// simply stays in sketchMembers and is emitted as an ordinary
// Sketch member below, which is a behavior difference from the
// original ONLY for a construct with zero confirmed real-sketch
// usage (see EnumScopeExtractor's javadoc for the corpus check).
// Hoist template-related items that cannot legally be Sketch members.
// Type specializations (TypeName etc.) must be at file scope.
// VariableDecl with templated types (Constant<42> c1) must also be hoisted.
// Variable templates (constexpr T pi = T(3.14)) can't be struct members.
{
List templateScopeItems = new ArrayList<>();
List remaining = new ArrayList<>();
for (TopLevelItem item : sketchMembers) {
boolean isTypeSpec = item instanceof TypeDef td && td.name().contains("<");
boolean isTemplatedVar = item instanceof VariableDecl vd
&& !vd.templateParams().isEmpty();
boolean isTemplateInstVar = item instanceof VariableDecl vd2
&& vd2.type() instanceof processing.mode.cpp.NamedType nt
&& !nt.templateArgs().isEmpty();
if (isTypeSpec || isTemplatedVar || isTemplateInstVar) {
templateScopeItems.add(item);
} else {
remaining.add(item);
}
}
for (TopLevelItem item : templateScopeItems) out.append(CodeGen.generateNode(item, 0));
sketchMembers = remaining;
}
// Strip user-written forward declarations from sketchMembers when
// a bodied definition of the same function also exists. Inside a
// C++ struct, members have full mutual visibility -- a forward decl
// is not only redundant but a redeclaration error if the definition
// is also a member. Found via Follow1.pde: "cannot be overloaded with
// 'void Sketch::segment(float,float,float)'" -- the user wrote
// "void segment(float x, float y, float a);" to enable forward-calling
// in plain C style, which is fine at file scope but illegal inside a class.
Set definedFunctions = new java.util.HashSet<>();
for (TopLevelItem item : sketchMembers) {
if (item instanceof FunctionDecl fd && fd.body() != null) {
definedFunctions.add(fd.name() + "/" + fd.params().size());
}
}
sketchMembers.removeIf(item ->
item instanceof FunctionDecl fd
&& fd.body() == null
&& !fd.isPureVirtual()
&& definedFunctions.contains(fd.name() + "/" + fd.params().size())
);
// Hoist TopLevelStatements (structured bindings, explicit instantiations) to namespace scope
List nsHoisted = new ArrayList<>();
List realMembers = new ArrayList<>();
for (TopLevelItem item : sketchMembers) {
if (item instanceof TopLevelStatement) {
nsHoisted.add(item);
} else {
realMembers.add(item);
}
}
for (TopLevelItem item : nsHoisted) out.append(CodeGen.generateNode(item, 0));
out.append("struct Sketch : public PApplet {\n");
for (TopLevelItem item : realMembers) {
out.append(CodeGen.generateNode(item, 1));
}
out.append("};\n");
}
out.append("void setup() { if(PApplet::g_papplet) PApplet::g_papplet->setup(); }\n");
out.append("void draw() { if(PApplet::g_papplet) PApplet::g_papplet->draw(); }\n");
out.append("void settings() { if(PApplet::g_papplet) PApplet::g_papplet->settings(); }\n");
out.append("} // namespace Processing\n");
out.append("\n");
out.append("#ifdef _WIN32\n");
out.append("#include \n");
out.append("int WINAPI WinMain(HINSTANCE,HINSTANCE,LPSTR,int) {\n");
out.append(" Processing::Sketch sketch;\n");
out.append(" sketch.run();\n");
out.append(" return 0;\n");
out.append("}\n");
out.append("#else\n");
out.append("int main(int argc, char** argv) {\n");
out.append(" Processing::Sketch sketch;\n");
out.append(" sketch.run();\n");
out.append(" return 0;\n");
out.append("}\n");
out.append("#endif\n");
File dest = new File(buildDir, "Sketch_run.cpp");
String outStr = out.toString();
// Restore noexcept on coroutine promise methods (parser drops noexcept specifier)
outStr = outStr.replaceAll("(\\bfinal_suspend\\b[^{;]*)(\\{)", "$1noexcept $2");
outStr = outStr.replaceAll("(\\binitial_suspend\\b[^{;]*)(\\{)", "$1noexcept $2");
Files.writeString(dest.toPath(), outStr);
return dest;
}
// Hoist only class/struct definitions (same logic as hoistClasses but returns just the class blocks)
/**
* Structured representation of one top-level declaration found by
* classifyTopLevelDecls(). Every later hoisting pass (classes, enums,
* functions, arrays, plain variables) should consume THIS shared list
* instead of re-scanning raw text with its own bespoke regex -- the
* scanning (comment/string-skipping, brace/bracket-depth tracking) is
* done exactly ONCE here, correctly, and every downstream pass just
* filters/reads already-extracted fields instead of re-deriving them.
*/
static class TopLevelDecl {
enum Kind { CLASS_OR_STRUCT, ENUM, FUNCTION, ARRAY_VAR, PLAIN_VAR, OBJECT_DIRECT_INIT, OTHER }
Kind kind;
String fullText; // exact original source text of this declaration, including trailing ;
String typeName; // element/return type (best-effort)
String name; // variable/function/class name
String sizeExpr; // ARRAY_VAR only: text inside [ ] -- empty string, a digit, or an identifier
boolean isConst; // PLAIN_VAR only: declared with const/constexpr/static (any combination)
int startPos, endPos; // position in the source this was found in
}
/**
* Walk `code` once, character-by-character, skipping comments and
* string/char literals correctly, and classify every top-level
* (4-space-indented, i.e. inside struct Sketch) statement into a
* TopLevelDecl. This does NOT hoist anything itself -- it just produces
* a structured list that hoisting passes can filter and act on without
* needing their own regex.
*/
/**
* Returns a copy of `code` with the CONTENTS of every line comment,
* block comment, and string/char literal replaced by spaces of the
* same length (newlines preserved as-is). Every character's POSITION
* is unchanged -- only what's inside comments/literals is blanked out.
*
* This exists so that any regex-based scan (the kind CppBuild has many
* of, for detecting things like "name = new Type(...)" assignments)
* can run against the RETURN VALUE of this function instead of raw
* `code`, and be automatically immune to false-positive matches inside
* comments or string literals -- e.g. a comment that says
* "// like obj = new Foo()" can no longer be mistaken for a real
* assignment statement, since by the time the regex runs, that text
* has already been replaced with blank spaces.
*
* This does NOT replace classifyTopLevelDecls() for detecting actual
* DECLARATIONS (which needs real structural parsing -- brace/bracket
* depth, statement boundaries) -- it's a lighter-weight tool for scans
* that only need "find text matching this shape, but not inside a
* comment or string," which covers most of CppBuild's regex-based
* passes that aren't already using the classifier.
*/
private static String blankCommentsAndLiterals(String code) {
StringBuilder out = new StringBuilder(code.length());
int n = code.length(), i = 0;
while (i < n) {
char c = code.charAt(i);
if (c == '/' && i+1 < n && code.charAt(i+1) == '/') {
int eol = code.indexOf('\n', i);
int end = (eol < 0) ? n : eol;
for (int k = i; k < end; k++) out.append(' ');
i = end;
continue;
}
if (c == '/' && i+1 < n && code.charAt(i+1) == '*') {
int close = code.indexOf("*/", i+2);
int end = (close < 0) ? n : close + 2;
for (int k = i; k < end; k++) out.append(code.charAt(k) == '\n' ? '\n' : ' ');
i = end;
continue;
}
if (c == '"') {
int j = i + 1;
while (j < n && code.charAt(j) != '"') { if (code.charAt(j) == '\\') j++; j++; }
int end = Math.min(j + 1, n);
for (int k = i; k < end; k++) out.append(code.charAt(k) == '\n' ? '\n' : ' ');
i = end;
continue;
}
if (c == '\'') {
int j = i + 1;
while (j < n && code.charAt(j) != '\'') { if (code.charAt(j) == '\\') j++; j++; }
int end = Math.min(j + 1, n);
for (int k = i; k < end; k++) out.append(code.charAt(k) == '\n' ? '\n' : ' ');
i = end;
continue;
}
out.append(c);
i++;
}
return out.toString();
}
private static java.util.List classifyTopLevelDecls(String code) {
java.util.List result = new java.util.ArrayList<>();
int n = code.length(), i = 0;
while (i < n) {
// Skip blank/whitespace-only stretches between statements
int lineStart = i;
// Only consider lines starting with exactly 4-space indent (Sketch body)
if (i + 4 > n || !code.startsWith(" ", i) ||
(i > 0 && code.charAt(i-1) != '\n' && i != 0)) {
i++;
continue;
}
// Confirm this position is truly the start of a line
if (i > 0 && code.charAt(i-1) != '\n') { i++; continue; }
int contentStart = i + 4;
if (contentStart >= n) break;
char c0 = code.charAt(contentStart);
// Skip comment-only or blank lines entirely
if (c0 == '\n') { i = contentStart + 1; continue; }
if (c0 == '/' && contentStart+1 < n && code.charAt(contentStart+1) == '/') {
int eol = code.indexOf('\n', contentStart);
i = (eol < 0) ? n : eol + 1;
continue;
}
// Skip a block comment that starts this line, INCLUDING multi-line
// ones (e.g. a "/**...*/" Javadoc-style header comment at the top of
// a sketch). Without this, the comment's opening line is treated as
// the start of "the next statement," and since the main scanning
// loop below skips comments internally without resetting what
// counts as the declaration's start, the comment and the REAL
// following statement (e.g. "const int num = 60;") get merged into
// one unrecognized OTHER blob -- making that constant invisible to
// every later pass that needs to find it by name.
if (c0 == '/' && contentStart+1 < n && code.charAt(contentStart+1) == '*') {
int end = code.indexOf("*/", contentStart+2);
i = (end < 0) ? n : end + 2;
while (i < n && code.charAt(i) != '\n' && (code.charAt(i)==' '||code.charAt(i)=='\t')) i++;
if (i < n && code.charAt(i) == '\n') i++;
continue;
}
// Find the end of this logical statement: walk forward tracking
// (), [], {}, and skipping strings/chars/comments, until we hit a
// top-level ';' or '{' (the latter meaning class/enum/function body).
int depthParen = 0, depthBracket = 0, depthBrace = 0;
int j = contentStart;
int firstBrace = -1, firstBracket = -1, firstParen = -1;
while (j < n) {
char c = code.charAt(j);
if (c == '/' && j+1 < n && code.charAt(j+1) == '/') {
int eol = code.indexOf('\n', j); j = (eol < 0) ? n : eol; continue;
}
if (c == '/' && j+1 < n && code.charAt(j+1) == '*') {
int end = code.indexOf("*/", j+2); j = (end < 0) ? n : end + 2; continue;
}
if (c == '"') {
j++; while (j < n && code.charAt(j) != '"') { if (code.charAt(j) == '\\') j++; j++; } j++; continue;
}
if (c == '\'') {
j++; while (j < n && code.charAt(j) != '\'') { if (code.charAt(j) == '\\') j++; j++; } j++; continue;
}
if (c == '(') { if (depthParen==0 && firstParen<0) firstParen=j; depthParen++; }
else if (c == ')') depthParen--;
else if (c == '[') { if (depthBracket==0 && firstBracket<0) firstBracket=j; depthBracket++; }
else if (c == ']') depthBracket--;
else if (c == '{') {
if (depthBrace==0 && firstBrace<0) firstBrace=j;
depthBrace++;
} else if (c == '}') {
depthBrace--;
if (depthBrace == 0 && firstBrace >= 0) { j++; break; } // end of a {...} body
} else if (c == ';' && depthParen==0 && depthBracket==0 && depthBrace==0) {
j++; break; // end of a simple statement
}
j++;
}
// Consume trailing ';' after a '{...}' body if present (class/struct Foo {...};)
int stmtEnd = j;
{ int k = stmtEnd; while (k < n && (code.charAt(k)==' '||code.charAt(k)=='\t')) k++;
if (k < n && code.charAt(k) == ';') stmtEnd = k + 1; }
String stmtText = code.substring(contentStart, stmtEnd);
String fullLineText = code.substring(lineStart, stmtEnd);
// Consume the trailing newline too, if present, so re-insertion is clean
int afterEnd = stmtEnd;
if (afterEnd < n && code.charAt(afterEnd) == '\n') afterEnd++;
TopLevelDecl d = new TopLevelDecl();
d.fullText = code.substring(lineStart, afterEnd);
d.startPos = lineStart;
d.endPos = afterEnd;
String trimmed = stmtText.strip();
if (trimmed.startsWith("class ") || trimmed.startsWith("struct ")) {
d.kind = TopLevelDecl.Kind.CLASS_OR_STRUCT;
java.util.regex.Matcher nm = java.util.regex.Pattern.compile("^(?:class|struct)\\s+([A-Za-z_]\\w*)").matcher(trimmed);
if (nm.find()) d.name = nm.group(1);
} else if (trimmed.startsWith("enum ")) {
d.kind = TopLevelDecl.Kind.ENUM;
java.util.regex.Matcher nm = java.util.regex.Pattern.compile("^enum(?:\\s+class|\\s+struct)?\\s+([A-Za-z_]\\w*)").matcher(trimmed);
if (nm.find()) d.name = nm.group(1);
} else if (firstParen >= 0 && firstBrace >= 0 && firstParen < firstBrace
&& (firstBracket < 0 || firstParen < firstBracket)) {
// "RetType name(...) { ... }" -- a function definition
d.kind = TopLevelDecl.Kind.FUNCTION;
java.util.regex.Matcher nm = java.util.regex.Pattern.compile(
"^(?:[A-Za-z_][\\w:<>,\\s\\*&]*?)\\s+([A-Za-z_]\\w*)\\s*\\(").matcher(trimmed);
if (nm.find()) d.name = nm.group(1);
} else if (firstBracket >= 0 && (firstBrace < 0 || firstBracket < firstBrace)
&& (firstParen < 0 || firstBracket < firstParen)) {
// "Type name[sizeExpr];" or "Type name[sizeExpr] = {...};" -- an array
d.kind = TopLevelDecl.Kind.ARRAY_VAR;
java.util.regex.Matcher am = java.util.regex.Pattern.compile(
"^([A-Za-z_]\\w*)\\s+([A-Za-z_]\\w*)\\s*\\[\\s*([A-Za-z_0-9]*)\\s*\\]").matcher(trimmed);
if (am.find()) { d.typeName = am.group(1); d.name = am.group(2); d.sizeExpr = am.group(3); }
} else if (firstParen >= 0 && firstBrace < 0) {
// "Type name(arg1, arg2, ...);" -- C++'s "most vexing parse":
// direct-initialization syntax is grammatically indistinguishable
// from a member-FUNCTION DECLARATION at this scope, and C++
// resolves the ambiguity in favor of "it's a function declaration"
// every time. Detected here so a later pass can rewrite the parens
// to braces (direct-list-initialization), which has no such
// ambiguity in C++.
d.kind = TopLevelDecl.Kind.OBJECT_DIRECT_INIT;
java.util.regex.Matcher om = java.util.regex.Pattern.compile(
"^([A-Za-z_]\\w*)\\s+([A-Za-z_]\\w*)\\s*\\(").matcher(trimmed);
if (om.find()) { d.typeName = om.group(1); d.name = om.group(2); }
} else {
// Plain variable declaration: "Type name [= expr];"
java.util.regex.Matcher vm = java.util.regex.Pattern.compile(
"^((?:const\\s+|constexpr\\s+|static\\s+)*)([A-Za-z_][\\w:<>,\\s\\*&]*?)\\s+([A-Za-z_]\\w*)\\s*(?:=.*)?$").matcher(trimmed.replaceAll(";\\s*$",""));
if (vm.find()) {
d.kind = TopLevelDecl.Kind.PLAIN_VAR;
d.isConst = !vm.group(1).isBlank();
d.typeName = vm.group(2);
d.name = vm.group(3);
} else {
d.kind = TopLevelDecl.Kind.OTHER;
}
}
result.add(d);
i = afterEnd;
}
return result;
}
private String hoistClassesOnly(String code) {
java.util.List classBlocks = new java.util.ArrayList<>();
int n = code.length(), i = 0;
while (i < n) {
if (i+1 < n && code.charAt(i)=='/' && code.charAt(i+1)=='/') {
while (i < n && code.charAt(i) != '\n') i++; continue;
}
if (i+1 < n && code.charAt(i)=='/' && code.charAt(i+1)=='*') {
i += 2; while (i+1 < n && !(code.charAt(i)=='*' && code.charAt(i+1)=='/')) i++; i += 2; continue;
}
if (code.charAt(i)=='"') {
i++; while (i < n && code.charAt(i)!='"') { if (code.charAt(i)=='\\') i++; i++; } i++; continue;
}
if (code.charAt(i)=='\'') {
i++; while (i < n && code.charAt(i)!='\'') { if (code.charAt(i)=='\\') i++; i++; } i++; continue;
}
// Plain "enum Name { ... };" (no "class"/"struct" after "enum") must
// still be walked-past correctly here, even though it isn't hoisted by
// THIS scanner (enumScope, extracted later from restCode, owns enums).
// Without this, the scanner has no concept of "enum" at all, leaves
// its position pointer sitting in the middle of the enum's body text,
// and the next "class"/"struct" token it stumbles across downstream
// gets mistakenly treated as starting exactly where the scan resumed --
// silently corrupting both the enum and whatever class came after it.
boolean isPlainEnum = i+5 <= n && code.substring(i, Math.min(i+5,n)).equals("enum ")
&& !(i+11 <= n && code.substring(i+5, Math.min(i+11,n)).equals("class "))
&& !(i+12 <= n && code.substring(i+5, Math.min(i+12,n)).equals("struct "));
if (isPlainEnum) {
char before = i > 0 ? code.charAt(i-1) : ' ';
if (!Character.isLetterOrDigit(before) && before != '_') {
int open = i;
while (open < n && code.charAt(open) != '{' && code.charAt(open) != ';') open++;
if (open < n && code.charAt(open) == '{') {
int depth = 0, k = open;
while (k < n) {
char c = code.charAt(k);
if (c=='"') { k++; while (k 0 ? code.charAt(i-1) : ' ';
char after = i+klen < n ? code.charAt(i+klen) : ' ';
// Skip "enum class"/"enum struct" -- handled separately as enumScope
boolean precededByEnum = i >= 5 && code.substring(i-5,i).equals("enum ");
if (!precededByEnum &&
!Character.isLetterOrDigit(before) && before != '_' &&
!Character.isLetterOrDigit(after) && after != '_') {
int open = i;
while (open < n && code.charAt(open) != '{' && code.charAt(open) != ';') open++;
if (open < n && code.charAt(open) == '{') {
int depth = 0, k = open;
while (k < n) {
char c = code.charAt(k);
if (c=='"') { k++; while (k prefix if it immediately precedes this class
String classText = code.substring(i, end);
// Java/Processing classes don't require a trailing ";" after the
// closing brace; C++ does. If the source omitted it, add one now so
// the hoisted class is valid standalone C++.
if (!classText.strip().endsWith(";")) classText = classText.stripTrailing() + ";\n";
String templatePrefix = "";
// Look back in code for a template<...> line immediately before position i
int lookback = i - 1;
while (lookback > 0 && (code.charAt(lookback)==' ' || code.charAt(lookback)=='\n' || code.charAt(lookback)=='\r')) lookback--;
int lineEnd = lookback;
while (lookback > 0 && code.charAt(lookback) != '\n') lookback--;
String prevLine = code.substring(lookback > 0 ? lookback+1 : 0, lineEnd+1).strip();
if (prevLine.startsWith("template")) {
templatePrefix = prevLine + "\n";
}
classBlocks.add(templatePrefix + classText);
i = end; continue;
}
}
}
i++;
}
// Sort: base classes before derived
boolean changed = true;
for (int pass = 0; pass < classBlocks.size()*2 && changed; pass++) {
changed = false;
for (int a = 0; a < classBlocks.size(); a++) {
String blockA = classBlocks.get(a);
java.util.regex.Matcher mA = java.util.regex.Pattern.compile(
"(?:class|struct)\\s+\\w+\\s*:\\s*(?:public|protected|private)\\s+(\\w+)").matcher(blockA);
if (!mA.find()) continue;
String aBase = mA.group(1);
for (int b = a+1; b < classBlocks.size(); b++) {
String blockB = classBlocks.get(b);
java.util.regex.Matcher mB = java.util.regex.Pattern.compile(
"(?:class|struct)\\s+(\\w+)").matcher(blockB);
if (mB.find() && mB.group(1).equals(aBase)) {
classBlocks.set(a, blockB); classBlocks.set(b, blockA);
changed = true; break;
}
}
if (changed) break;
}
}
StringBuilder out = new StringBuilder();
for (String b : classBlocks) out.append(b).append("\n\n");
return out.toString();
}
// Remove the class blocks that hoistClassesOnly() extracted, leaving the rest
private String removeHoistedClasses(String code) {
// Re-run the same extraction logic and blank out those ranges
java.util.List ranges = new java.util.ArrayList<>();
int n = code.length(), i = 0;
while (i < n) {
if (i+1 < n && code.charAt(i)=='/' && code.charAt(i+1)=='/') {
while (i < n && code.charAt(i) != '\n') i++; continue;
}
if (i+1 < n && code.charAt(i)=='/' && code.charAt(i+1)=='*') {
i += 2; while (i+1 < n && !(code.charAt(i)=='*' && code.charAt(i+1)=='/')) i++; i += 2; continue;
}
if (code.charAt(i)=='"') {
i++; while (i < n && code.charAt(i)!='"') { if (code.charAt(i)=='\\') i++; i++; } i++; continue;
}
if (code.charAt(i)=='\'') {
i++; while (i < n && code.charAt(i)!='\'') { if (code.charAt(i)=='\\') i++; i++; } i++; continue;
}
// Plain "enum Name { ... };" (no "class"/"struct" after "enum") must
// still be walked-past correctly here, even though it isn't hoisted by
// THIS scanner (enumScope, extracted later from restCode, owns enums).
// Without this, the scanner has no concept of "enum" at all, leaves
// its position pointer sitting in the middle of the enum's body text,
// and the next "class"/"struct" token it stumbles across downstream
// gets mistakenly treated as starting exactly where the scan resumed --
// silently corrupting both the enum and whatever class came after it.
boolean isPlainEnum = i+5 <= n && code.substring(i, Math.min(i+5,n)).equals("enum ")
&& !(i+11 <= n && code.substring(i+5, Math.min(i+11,n)).equals("class "))
&& !(i+12 <= n && code.substring(i+5, Math.min(i+12,n)).equals("struct "));
if (isPlainEnum) {
char before = i > 0 ? code.charAt(i-1) : ' ';
if (!Character.isLetterOrDigit(before) && before != '_') {
int open = i;
while (open < n && code.charAt(open) != '{' && code.charAt(open) != ';') open++;
if (open < n && code.charAt(open) == '{') {
int depth = 0, k = open;
while (k < n) {
char c = code.charAt(k);
if (c=='"') { k++; while (k 0 ? code.charAt(i-1) : ' ';
char after = i+klen < n ? code.charAt(i+klen) : ' ';
// Skip "enum class"/"enum struct" -- handled separately as enumScope
boolean precededByEnum = i >= 5 && code.substring(i-5,i).equals("enum ");
if (!precededByEnum &&
!Character.isLetterOrDigit(before) && before != '_' &&
!Character.isLetterOrDigit(after) && after != '_') {
int open = i;
while (open < n && code.charAt(open) != '{' && code.charAt(open) != ';') open++;
if (open < n && code.charAt(open) == '{') {
int depth = 0, k = open;
while (k < n) {
char c = code.charAt(k);
if (c=='"') { k++; while (k" line (for template
// functions/classes), so it gets blanked out along with the class
// body instead of being left as an orphaned, dangling header line
// in restCode once the class itself has been removed.
int rangeStart = i;
{
int lookback = i - 1;
while (lookback > 0 && (code.charAt(lookback)==' ' || code.charAt(lookback)=='\n' || code.charAt(lookback)=='\r')) lookback--;
int lineEndLB = lookback;
while (lookback > 0 && code.charAt(lookback) != '\n') lookback--;
int lineStartLB = lookback > 0 ? lookback + 1 : 0;
String prevLineLB = code.substring(lineStartLB, lineEndLB + 1).strip();
if (prevLineLB.startsWith("template")) {
rangeStart = lineStartLB;
}
}
ranges.add(new int[]{rangeStart, end});
i = end; continue;
}
}
}
i++;
}
if (ranges.isEmpty()) return code;
StringBuilder sb = new StringBuilder();
int pos = 0;
for (int[] r : ranges) {
sb.append(code, pos, r[0]);
pos = r[1];
}
sb.append(code, pos, n);
return sb.toString();
}
// Rewrite free function definitions as Sketch struct methods.
// Lifecycle functions get "override" appended. Everything else becomes a method too.
// Variable declarations at top level become struct fields.
/**
* Remove _PSketch injection from pure data structs (structs with only fields, no methods).
* A struct is "pure data" if it has no method bodies â detected by absence of
* return-type method signatures inside the struct body.
*/
private String removePSketchFromDataStructs(String code) {
// Parse each struct/class block and check if it has methods
StringBuilder result = new StringBuilder();
int i = 0;
while (i < code.length()) {
// Look for "_PSketch {" pattern
int psk = code.indexOf(": public virtual _PSketch {", i);
if (psk < 0) { result.append(code.substring(i)); break; }
// Find the struct keyword before this
int lineStart = code.lastIndexOf("\n", psk) + 1;
String beforeBrace = code.substring(lineStart, psk);
// Find the matching closing brace
int braceOpen = psk + ": public virtual _PSketch {".length() - 1;
int depth = 1, j = braceOpen + 1;
while (j < code.length() && depth > 0) {
if (code.charAt(j) == '{') depth++;
else if (code.charAt(j) == '}') depth--;
j++;
}
String body = code.substring(braceOpen + 1, j - 1);
// Check if body has any method signatures (return type + name + '(')
// A method has: word whitespace word '(' â e.g. "void update(" or "float distTo("
boolean hasMethod = body.matches("(?s).*\\b(void|int|float|bool|double|color|auto|std::\\w+)\\s+\\w+\\s*\\(.*")
&& !body.contains("constexpr"); // skip constexpr structs
result.append(code, i, lineStart);
// Also remove _PSketch from template classes -- they can't use Processing API
boolean isTemplate = lineStart > 0 && code.substring(Math.max(0, lineStart - 200), lineStart).contains("template<");
if (hasMethod && !isTemplate) {
// Keep _PSketch injection
result.append(code, lineStart, j);
} else {
// Remove _PSketch â revert to plain struct/class name {
result.append(beforeBrace).append(" {").append(body).append("}");
}
i = j;
}
return result.toString();
}
private String rewriteAsSketchMethods(String code, String[] lifecycleMethods) {
java.util.Set lifecycle = new java.util.HashSet<>(java.util.Arrays.asList(lifecycleMethods));
// Indent each line by 4 spaces (they're now inside struct Sketch { ... })
// Lifecycle functions: add "override" to their signature
StringBuilder out = new StringBuilder();
String[] lines = code.split("\n", -1);
for (String line : lines) {
String trimmed = line.strip();
String indented = " " + line;
// Initialize pointer fields to nullptr
if (indented.matches("\\s+\\w[\\w:<>*,\\s]+\\*\\s+\\w+\\s*;")) {
indented = indented.replaceFirst(";\\s*$", " = nullptr;");
}
// Detect lifecycle function definition lines and add "override"
for (String lc : lifecycle) {
// Match: void setup() { or void setup() \n (with optional spaces)
// Pattern: void ( at start of trimmed line (not inside a longer name)
java.util.regex.Pattern p = java.util.regex.Pattern.compile(
"^(\\s*(?:virtual\\s+)?void\\s+" + java.util.regex.Pattern.quote(lc) + "\\s*\\([^)]*\\)\\s*)\\{(.*)\\}\\s*$");
java.util.regex.Matcher m = p.matcher(line);
if (m.find()) {
String bodyContent = m.group(2).strip();
if (bodyContent.isEmpty()) {
indented = " " + m.group(1) + "override {}";
} else {
// Single-line function with body â expand to multi-line
indented = " " + m.group(1) + "override {\n " + bodyContent + "\n }";
}
break;
}
// Match opening brace only (multi-line body follows)
java.util.regex.Pattern p1b = java.util.regex.Pattern.compile(
"^(\\s*(?:virtual\\s+)?void\\s+" + java.util.regex.Pattern.quote(lc) + "\\s*\\([^)]*\\)\\s*)\\{\\s*$");
java.util.regex.Matcher m1b = p1b.matcher(line);
if (m1b.find()) {
indented = " " + m1b.group(1) + "override {";
break;
}
// Also handle: void mouseWheel(int delta) {
java.util.regex.Pattern p2 = java.util.regex.Pattern.compile(
"^(\\s*(?:virtual\\s+)?void\\s+" + java.util.regex.Pattern.quote(lc) + "\\s*\\([^)]*\\)\\s*)$");
java.util.regex.Matcher m2 = p2.matcher(line);
if (m2.find()) {
indented = " " + m2.group(1) + "override";
break;
}
}
out.append(indented).append("\n");
}
return out.toString();
}
// Extract class/struct definitions and move them to the top, sorted by inheritance.
private String hoistClasses(String code) {
java.util.List classBlocks = new java.util.ArrayList<>();
StringBuilder rest = new StringBuilder();
int n = code.length(), i = 0;
while (i < n) {
if (i+1 < n && code.charAt(i)=='/' && code.charAt(i+1)=='/') {
while (i < n && code.charAt(i) != '\n') i++; continue;
}
if (i+1 < n && code.charAt(i)=='/' && code.charAt(i+1)=='*') {
i += 2; while (i+1 < n && !(code.charAt(i)=='*' && code.charAt(i+1)=='/')) i++; i += 2; continue;
}
if (code.charAt(i)=='"') {
i++; while (i < n && code.charAt(i)!='"') { if (code.charAt(i)=='\\') i++; i++; } i++; continue;
}
if (code.charAt(i)=='\'') {
i++; while (i < n && code.charAt(i)!='\'') { if (code.charAt(i)=='\\') i++; i++; } i++; continue;
}
boolean isClass = i+5 <= n && code.substring(i, Math.min(i+5,n)).equals("class");
boolean isStruct = i+6 <= n && code.substring(i, Math.min(i+6,n)).equals("struct");
if (isClass || isStruct) {
int klen = isClass ? 5 : 6;
char before = i > 0 ? code.charAt(i-1) : ' ';
char after = i+klen < n ? code.charAt(i+klen) : ' ';
if (!Character.isLetterOrDigit(before) && before != '_' &&
!Character.isLetterOrDigit(after) && after != '_') {
int open = i;
while (open < n && code.charAt(open) != '{' && code.charAt(open) != ';') open++;
if (open < n && code.charAt(open) == '{') {
int depth = 0, k = open;
while (k < n) {
char c = code.charAt(k);
if (c=='"') { k++; while (k]\\s*$\\n?", "")
.replaceAll("(?m)^\\s*#include\\s+[\"<]Processing\\.cpp[\">]\\s*$\\n?", "");
}
private String stripNamespaceProcessing(String code) {
Pattern p = Pattern.compile("namespace\\s+Processing\\s*\\{", Pattern.MULTILINE);
Matcher m = p.matcher(code);
if (!m.find()) return code;
int open = code.indexOf('{', m.start());
if (open < 0) return code;
int depth = 0, close = -1;
for (int i = open; i < code.length(); i++) {
char c = code.charAt(i);
if (c == '{') depth++;
else if (c == '}' && --depth == 0) { close = i; break; }
}
if (close < 0) return code;
return code.substring(0, m.start())
+ code.substring(open + 1, close)
+ code.substring(close + 1);
}
private String sanitize(String code) {
if (code.startsWith("\uFEFF")) code = code.substring(1);
return code
.replace('\u2018', '\'').replace('\u2019', '\'')
.replace('\u201C', '"') .replace('\u201D', '"')
.replace('\u2013', '-') .replace('\u2014', '-');
}
// Replace keywords only outside string/char literals and comments
private String replaceKeywordsOutsideLiterals(String code, String[][] replacements) {
StringBuilder out = new StringBuilder();
int i = 0, n = code.length();
while (i < n) {
char c = code.charAt(i);
// Line comment -- copy to end of line verbatim
if (c == '/' && i+1 < n && code.charAt(i+1) == '/') {
int end = code.indexOf('\n', i);
if (end < 0) end = n;
out.append(code, i, end);
i = end;
continue;
}
// Block comment -- copy verbatim
if (c == '/' && i+1 < n && code.charAt(i+1) == '*') {
int end = code.indexOf("*/", i+2);
if (end < 0) end = n; else end += 2;
out.append(code, i, end);
i = end;
continue;
}
// String literal -- copy verbatim
if (c == '"') {
out.append(c); i++;
while (i < n) {
char sc = code.charAt(i);
out.append(sc); i++;
if (sc == '\\' && i < n) { out.append(code.charAt(i++)); continue; }
if (sc == '"') break;
}
continue;
}
// Char literal -- copy verbatim
if (c == '\'') {
out.append(c); i++;
while (i < n) {
char sc = code.charAt(i);
out.append(sc); i++;
if (sc == '\\' && i < n) { out.append(code.charAt(i++)); continue; }
if (sc == '\'') break;
}
continue;
}
// Outside literals: check for keyword match
boolean matched = false;
for (String[] pair : replacements) {
String from = pair[0], to = pair[1];
int flen = from.length();
if (i + flen <= n && code.substring(i, i+flen).equals(from)) {
boolean leftOk = i == 0 || !Character.isLetterOrDigit(code.charAt(i-1)) && code.charAt(i-1) != '_';
boolean rightOk = i+flen >= n || !Character.isLetterOrDigit(code.charAt(i+flen)) && code.charAt(i+flen) != '_';
if (leftOk && rightOk) {
out.append(to);
i += flen;
matched = true;
break;
}
}
}
if (!matched) { out.append(c); i++; }
}
return out.toString();
}
// Detect variables assigned color()/lerpColor() and fix their type from int to color.
// Also propagates through function calls so params that receive color vars become color too.
private String fixColorTypes(String code) {
// Pass 1: find all variables directly assigned color()/lerpColor()
java.util.Set colorVars = new java.util.HashSet<>();
Pattern assignPat = Pattern.compile("\\b(\\w+)\\s*=\\s*(?:color|lerpColor)\\s*\\(");
Matcher am = assignPat.matcher(code);
while (am.find()) colorVars.add(am.group(1));
// Pass 2: propagate through function calls iteratively until stable
boolean changed = true;
while (changed) {
changed = false;
// Find function definitions
Pattern fnDef = Pattern.compile(
"(?:void|int|float|bool|color)\\s+(\\w+)\\s*\\(([^)]*)\\)",
Pattern.MULTILINE);
Matcher fd = fnDef.matcher(code);
while (fd.find()) {
String fnName = fd.group(1);
String params = fd.group(2);
// Extract param names
List paramNames = new ArrayList<>();
for (String p : params.split(",")) {
String[] parts = p.trim().split("\\s+");
if (parts.length >= 2) paramNames.add(parts[parts.length - 1].replaceAll("[\\[\\]*]",""));
}
if (paramNames.isEmpty()) continue;
// Find calls to this function
Pattern callPat = Pattern.compile("\\b" + Pattern.quote(fnName) + "\\s*\\(([^;{]*)\\)");
Matcher cm = callPat.matcher(code);
while (cm.find()) {
String[] args = cm.group(1).split(",");
for (int ai = 0; ai < args.length && ai < paramNames.size(); ai++) {
String arg = args[ai].trim();
if (colorVars.contains(arg) && !colorVars.contains(paramNames.get(ai))) {
colorVars.add(paramNames.get(ai));
changed = true;
}
}
}
}
}
// Pass 3: fix int arrays that hold color values
Pattern arrPat = Pattern.compile("\\bint\\s+(\\w+)\\[(\\d+)\\]\\s*=\\s*\\{([^}]*)\\}");
Matcher arr = arrPat.matcher(code);
StringBuffer sb3 = new StringBuffer();
while (arr.find()) {
boolean anyColor = false;
for (String a : arr.group(3).split(","))
if (colorVars.contains(a.trim())) { anyColor = true; break; }
if (anyColor) {
colorVars.add(arr.group(1));
arr.appendReplacement(sb3, "color " + arr.group(1) + "[" + arr.group(2) + "] = {" + arr.group(3) + "}");
} else arr.appendReplacement(sb3, arr.group(0));
}
arr.appendTail(sb3);
code = sb3.toString();
// Pass 4: replace int varname declarations with color varname for all colorVars
// Skip variables declared as loop counters or with explicit int literals
// to avoid mistyping loop vars like "for (int c = 0; ...)" as color
java.util.regex.Pattern forLoopDecl = java.util.regex.Pattern.compile(
"\\bfor\\s*\\(\\s*int\\s+(\\w+)");
java.util.Set loopVars = new java.util.HashSet<>();
java.util.regex.Matcher lm = forLoopDecl.matcher(code);
while (lm.find()) loopVars.add(lm.group(1));
for (String v : colorVars) {
if (loopVars.contains(v)) continue; // skip loop counters
if (v.length() == 1) continue; // skip single-letter vars (likely loop counters or params)
code = code.replaceAll("\\bint\\s+(" + Pattern.quote(v) + ")\\b", "color $1");
}
return code;
}
/**
* Java/Processing has no value-vs-pointer distinction for user-defined
* types -- "Foo f; f = new Foo(...);" means f is a REFERENCE to a newly
* allocated Foo, and every later "f.method()"/"f.field" is reference
* access. A naive translation keeps "Foo f;" as a C++ VALUE declaration
* (since that\'s what the text literally says), but then "f = new Foo(...)"
* tries to assign a Foo* into a Foo, which fails to compile (no implicit
* pointer-to-value conversion, and most user classes correctly have
* copy-assignment either deleted or simply not viable for this).
*
* Fix: detect every variable that is ever assigned via "name = new Type(...)"
* anywhere in the source, rewrite its declaration from "Type name;" (or
* "Type nameA, nameB;") to "Type* name = nullptr;", and rewrite every
* later "name.member" access for that variable to "name->member" to match
* pointer semantics, exactly mirroring what Java\'s reference semantics
* actually meant in the first place.
*/
/**
* "Type name[] = { a, b, c };" is an ordinary, fixed-size global in
* Java/Processing -- but once CppBuild wraps every top-level global into
* a field of struct Sketch, a C-style array sized purely by its
* initializer becomes what C++ calls a "flexible array member," which is
* only legal as the LAST member of a struct, and never with an
* initializer at all. Any such declaration anywhere else (i.e. almost
* always, since sketches rarely declare exactly one array as their very
* last global) fails to compile.
*
* Fix: rewrite "Type name[] = { ...N items... };" to
* "std::array name = { ...N items... };", which has no such
* restriction as a class member. Also rewrite the common companion
* idiom "sizeof(name)/sizeof(name[0])" (used to compute element count)
* to "name.size()", since std::array exposes that directly and sizeof
* no longer makes sense once the type is no longer a raw C array.
*/
// [E0004] "Unsupported Java Syntax" category. Detects real Java
// array-declaration syntax -- "int[] a = new int[5];", any number of
// dimensions ("int[][] grid = new int[5][5];"), any element type
// (primitive or class name), and arbitrary whitespace between the
// type, the brackets, and the variable name (Java itself allows all
// of this; the regex below mirrors that flexibility rather than only
// matching the single most common spacing).
//
// This pattern is real, valid JAVA but is NOT valid C++ at all -- it
// fails to even parse as C++ (square brackets after a bare type name,
// before the variable name, isn't legal C++ grammar), so without this
// check the sketch author would see a confusing, unrelated low-level
// syntax error from g++ instead of a direct, actionable message
// pointing them at Array, the real CppMode way to write this.
//
// Scans a comment/string-blanked copy, not raw code, for the same
// reason every other text-pattern scan in this file does: a comment
// mentioning this exact syntax as an example shouldn't trigger a
// build-stopping error.
// Replaces the original regex-over-blanked-text scan with the real
// token-stream-based check (see CppJavaArrayCheck.java). Same external
// contract as before: detailed message to System.err (the persistent
// console text area), a short message to listener.statusError() (the
// volatile status bar), and a thrown RuntimeException to stop the build
// -- callers of this method don't need to change.
private String getWebsiteBaseUrl() {
try {
java.io.File configFile = new java.io.File(mode.getRuntimeDir().getParentFile(), "config/cppmode.properties");
if (configFile.exists()) {
java.util.Properties props = new java.util.Properties();
try (java.io.InputStream in = new java.io.FileInputStream(configFile)) { props.load(in); }
String url = props.getProperty("website.base.url", "").trim();
if (!url.isEmpty()) return url;
}
} catch (Exception e) {}
return "https://processing-cpp.github.io";
}
// [E0005] ArrayList.get() value-copy detection.
// Detects: "TypeName var = expr.get(...);" where TypeName is not a pointer.
// ArrayList stores T* internally so .get() returns T*, not T.
private void checkForArrayListGetValueCopy(String code, RunnerListener listener) {
List tokens;
try {
tokens = new CppLexer(code).tokenize();
} catch (Exception e) { return; }
for (int i = 0; i + 4 < tokens.size(); i++) {
CppLexerToken t0 = tokens.get(i);
CppLexerToken t1 = tokens.get(i + 1);
CppLexerToken t2 = tokens.get(i + 2);
// Skip known value-returning types: String (StringList.get returns string by value)
java.util.Set valueTypes = java.util.Set.of("String", "string");
if (t0.type() == CppLexerTokenType.IDENTIFIER
&& !valueTypes.contains(t0.text())
&& t1.type() == CppLexerTokenType.IDENTIFIER
&& t2.isOp("=")) {
int j = i + 3; int depth = 0; boolean foundGet = false;
while (j < tokens.size()) {
CppLexerToken tj = tokens.get(j);
if (tj.isPunct(";") && depth == 0) break;
if (tj.isPunct("(") || tj.isPunct("{")) depth++;
if (tj.isPunct(")") || tj.isPunct("}")) { if (--depth < 0) break; }
if (tj.isPunct(".") && j + 2 < tokens.size()
&& tokens.get(j + 1).text().equals("get")
&& tokens.get(j + 2).isPunct("(")) { foundGet = true; break; }
j++;
}
if (foundGet) {
int line = t0.line();
String url = getWebsiteBaseUrl() + "/error/E0005.html";
String msg =
"\n[E0005] ArrayList.get() returns T* (pointer), not T by value.\n" +
" Line " + line + ": \"" + t0.text() + " " + t1.text() + " = ...get(...);\"\n" +
" ArrayList stores objects as pointers internally.\n" +
" Fix: declare a pointer variable:\n" +
" " + t0.text() + "* " + t1.text() + " = list.get(i);\n" +
" Then use -> instead of .:\n" +
" " + t1.text() + "->field; " + t1.text() + "->method();\n" +
" Reference: " + url + "\n";
System.err.println(msg);
listener.statusError("E0005: ArrayList.get() returns pointer -- see console. " + url);
throw new AlreadyReportedException("E0005: ArrayList value-copy -- see console.");
}
}
}
}
// [E0005b] Detect inline .get().field pattern: particles.get(0).x
// This is a pointer dereference error -- should use -> not .
// Only fires for ArrayList (pointer-storage), not Array (value-storage).
// Distinguish by looking at the token before .get(): ArrayList uses
// pointer storage and get() returns T*; Array uses value storage and
// get() returns T by value, so .field is correct there.
private void checkForArrayListGetDotAccess(String code, RunnerListener listener) {
List tokens;
try { tokens = new CppLexer(code).tokenize(); } catch (Exception e) { return; }
for (int i = 0; i + 6 < tokens.size(); i++) {
// Pattern: .get( ... ) . IDENTIFIER (not "(") -- member access on get() result
if (!tokens.get(i).isPunct(".")) continue;
if (!tokens.get(i + 1).text().equals("get")) continue;
if (!tokens.get(i + 2).isPunct("(")) continue;
// Only fire E0005b when we can positively identify the receiver as ArrayList
// (pointer-storage). Array is value-storage and get() returns T by value,
// so .field is correct there. Since we have no symbol table, we can't resolve
// arbitrary expression types -- so we require a positive signal: the variable
// name immediately before .get( must appear in a nearby ArrayList<...> declaration.
// If we can't confirm ArrayList, skip -- false negatives are safer than false
// positives that block valid code.
{
// Find the identifier token immediately before this ".get("
// It may be: "name.get(" or "name[i].get(" or "fn().get(" etc.
// Walk back to find the base name token.
int back = i - 1;
while (back > 0 && (tokens.get(back).isPunct("]") || tokens.get(back).isPunct(")"))) {
String closeStr = tokens.get(back).text();
String openStr = closeStr.equals("]") ? "[" : "(";
int bd = 1; back--;
while (back >= 0 && bd > 0) {
if (tokens.get(back).text().equals(closeStr)) bd++;
else if (tokens.get(back).text().equals(openStr)) bd--;
back--;
}
}
String baseName = (back >= 0 && tokens.get(back).type() == CppLexerTokenType.IDENTIFIER)
? tokens.get(back).text() : null;
// Require positive confirmation: scan nearby tokens for "ArrayList < ... > baseName"
boolean confirmedArrayList = false;
if (baseName != null) {
for (int k = Math.max(0, i - 40); k < Math.min(tokens.size(), i + 5); k++) {
if (tokens.get(k).text().equals("ArrayList")
&& k + 1 < tokens.size() && tokens.get(k+1).isOp("<")) {
// Scan forward past the template args to find the variable name
int m = k + 2; int ad = 1;
while (m < tokens.size() && ad > 0) {
if (tokens.get(m).isOp("<")) ad++;
else if (tokens.get(m).isOp(">") || tokens.get(m).isOp(">>")) ad--;
m++;
}
// m now points just after ">", optionally "*", then the variable name
while (m < tokens.size() && tokens.get(m).isOp("*")) m++;
if (m < tokens.size() && tokens.get(m).text().equals(baseName)) {
confirmedArrayList = true; break;
}
}
}
}
if (!confirmedArrayList) continue; // can't confirm ArrayList -- skip
}
// Consume the get(...) args
int j = i + 3; int depth = 1;
while (j < tokens.size() && depth > 0) {
if (tokens.get(j).isPunct("(")) depth++;
else if (tokens.get(j).isPunct(")")) depth--;
j++;
}
// After get(...), check if "." follows (not "->")
if (j < tokens.size() && tokens.get(j).isPunct(".")
&& j + 1 < tokens.size()
&& tokens.get(j + 1).type() == CppLexerTokenType.IDENTIFIER) {
int line = tokens.get(i).line();
String field = tokens.get(j + 1).text();
String url = getWebsiteBaseUrl() + "/error/E0005.html";
String msg =
"\n[E0005] .get() returns T* (pointer) -- use -> not . to access members.\n" +
" Line " + line + ": \"...get(...)." + field + "\"\n" +
" Fix: use -> instead of .:\n" +
" list.get(i)->" + field + ";\n" +
" Or store as pointer first:\n" +
" Type* p = list.get(i); p->" + field + ";\n" +
" Reference: " + url + "\n";
System.err.println(msg);
listener.statusError("E0005: use -> not . after ArrayList.get() -- see console. " + url);
throw new AlreadyReportedException("E0005: .get().field -- see console.");
}
}
}
// [E0006] Java static method/field call syntax: ClassName.member must be ClassName::member in C++
//
// Generalized rule: any identifier that starts with an uppercase letter followed
// by a "." and then another identifier (with optional "(") is likely a Java
// static access pattern. This covers:
// Integer.parseInt(...) -> Integer::parseInt(...)
// Math.PI -> Math::PI (or just PI)
// MyClass.CONSTANT -> MyClass::CONSTANT
// Collections.sort(...) -> Collections::sort(...) or std::sort
//
// Excluded: known instance-access patterns where the variable name
// happens to start with uppercase (e.g. PImage, PFont, PVector variables).
private void checkForJavaStaticCallSyntax(String code, RunnerListener listener) {
List tokens;
try { tokens = new CppLexer(code).tokenize(); } catch (Exception e) { return; }
// Types known to be used as instances (not static-only classes)
java.util.Set instanceTypes = java.util.Set.of(
"PImage", "PFont", "PVector", "PShape", "PGraphics",
"ArrayList", "IntList", "FloatList", "StringList",
"String", "Array", "Table", "TableRow", "JSONObject", "JSONArray",
"PrintWriter", "BufferedReader", "XML"
);
// Definitely-static classes (always trigger E0006)
java.util.Set staticClasses = java.util.Set.of(
"Integer", "Float", "Double", "Long", "Boolean", "Character",
"Byte", "Short", "Math", "Arrays", "Collections", "System",
"Thread", "Runtime", "Class", "Objects"
);
for (int i = 0; i + 2 < tokens.size(); i++) {
CppLexerToken t0 = tokens.get(i);
CppLexerToken t1 = tokens.get(i + 1);
CppLexerToken t2 = tokens.get(i + 2);
// Pattern: IDENTIFIER . IDENTIFIER where first IDENTIFIER starts uppercase
if (t0.type() != CppLexerTokenType.IDENTIFIER) continue;
if (!t1.isPunct(".")) continue;
if (t2.type() != CppLexerTokenType.IDENTIFIER) continue;
String firstName = t0.text();
if (firstName.isEmpty() || !Character.isUpperCase(firstName.charAt(0))) continue;
if (instanceTypes.contains(firstName)) continue; // known instance type
// Single-letter uppercase = almost certainly a template param (T, S, U, K, V, etc.)
if (firstName.length() == 1) continue;
// Two-letter uppercase combos commonly used as template params: Ts, Vs, etc.
if (firstName.length() == 2 && Character.isLowerCase(firstName.charAt(1))) continue;
// Only fire for known static classes OR if preceded by nothing
// (i.e. not "obj.UpperMethod()" which is a method call)
// Check context: if previous token is ")", "]", or identifier, skip
boolean preceded = i > 0 && (
tokens.get(i-1).isPunct(")")
|| tokens.get(i-1).isPunct("]")
|| tokens.get(i-1).isPunct(">")
|| tokens.get(i-1).type() == CppLexerTokenType.IDENTIFIER);
if (preceded && !staticClasses.contains(firstName)) continue;
// Check that next token after t2 is "(" or nothing method-like
boolean isCall = i + 3 < tokens.size() && tokens.get(i + 3).isPunct("(");
boolean isField = !isCall; // field access like Math.PI
if (!staticClasses.contains(firstName) && !isCall) continue; // only fire on known statics for field access
int line = t0.line();
String url = getWebsiteBaseUrl() + "/error/E0006.html";
String access = t2.text() + (isCall ? "(...)" : "");
String msg =
"\n[E0006] Java static access syntax is not valid in C++.\n" +
" Line " + line + ": \"" + firstName + "." + access + "\"\n" +
" In C++, static members use :: not .:\n" +
" " + firstName + "::" + access + "\n" +
" Reference: " + url + "\n";
System.err.println(msg);
listener.statusError("E0006: use " + firstName + "::" + t2.text() + " not . -- see console");
throw new AlreadyReportedException("E0006: Java static access syntax -- see console.");
}
}
// [E0007] Variable name collides with Processing API function name
private void checkForProcessingNameCollisions(String code, RunnerListener listener) {
// Match variable declarations: "type name" or "type name =" at statement start
java.util.regex.Pattern varDecl = java.util.regex.Pattern.compile(
"\\b(?:int|float|bool|double|char|auto|color|String)\\s+("
+ String.join("|", PROCESSING_API_NAMES)
+ ")\\b");
java.util.regex.Matcher m = varDecl.matcher(code);
while (m.find()) {
String name = m.group(1);
// Find line number
int line = 1;
for (int i = 0; i < m.start(); i++) if (code.charAt(i) == '\n') line++;
String url = getWebsiteBaseUrl() + "/error/E0007.html";
String msg =
"\n[E0007] Variable name '" + name + "' shadows the Processing type 'color'.\n"
+ " Line " + line + ": this causes type inference errors. Rename to e.g. 'col', 'clr', or 'c2'.\n"
+ " Reference: " + url + "\n";
System.err.println(msg);
listener.statusError("E0007: '" + name + "' shadows Processing API -- see console");
throw new AlreadyReportedException("E0007: variable name shadows Processing API");
}
}
private void checkForUnsupportedJavaArraySyntax(String code, RunnerListener listener) {
try {
CppJavaArrayCheck.check(code);
} catch (CppJavaArrayCheck.E0004Exception e) {
System.err.println(e.getMessage());
listener.statusError("E0004: Unsupported Java array syntax -- see console for details.");
throw new AlreadyReportedException("E0004: Unsupported Java array syntax -- see console for details.");
}
}
/**
* Expands Java's diamond operator in common patterns:
* "ArrayList x = new ArrayList<>();" -> "ArrayList x = new ArrayList();"
* Scans line by line; extracts the LHS template args and inserts them into <> on RHS.
*/
private String expandDiamondOperator(String code) {
// "ArrayList x = new ArrayList<>();" -> "ArrayList x = ArrayList();"
// Pattern: TypeName varName = new TypeName<>()
java.util.regex.Pattern p = java.util.regex.Pattern.compile(
"(\\w+)(<[^<>]+>)(\\s+\\w+\\s*=\\s*)new\\s+\\w+<>\\s*\\(");
java.util.regex.Matcher m = p.matcher(code);
StringBuffer sb = new StringBuffer();
while (m.find()) {
// Replace "new TypeName<>(" with "TypeName("
String replacement = m.group(1) + m.group(2) + m.group(3) + m.group(1) + m.group(2) + "(";
m.appendReplacement(sb, java.util.regex.Matcher.quoteReplacement(replacement));
}
m.appendTail(sb);
// Simpler line-by-line approach for the common case
String result = sb.toString();
// Replace any remaining "new TypeName<>()" with "new TypeName()" as fallback
result = result.replaceAll("new (\\w+)<>\\(", "new $1(");
return result;
}
private String javaToC(String code) {
// Expand diamond operator: "ArrayList x = new ArrayList<>()" -> "new ArrayList()"
// Scan for pattern: TypeName varName = new TypeName<>();
code = expandDiamondOperator(code);
// Strip "new" from value-type assignments: "ArrayList x = new ArrayList()"
// Only when the declared type and new type have the same base name (Java idiom).
// Do NOT strip when types differ (e.g. "node_ptr n = new node_type(val)")
code = stripMatchingNew(code);
// Run color type propagation first
code = fixColorTypes(code);
// âÂÂâ 1. Whole-word keyword replacements (skip string/char literals) âÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂ
String[][] words = {
{ "boolean", "bool" },
// Integer/Float/Double/Long/Byte/Character are intentionally NOT
// rewritten to their primitive equivalents here -- they're real
// Java wrapper classes (Processing.h provides matching C++ wrapper
// classes for the same reason String does: real boxing semantics
// like nullability, valueOf()/parseXxx() static factories, and
// xxxValue() accessors, none of which a bare "int"/"float"/etc.
// can express). Rewriting these to plain primitives as text was
// the same category of bug as the old String->std::string rename
// -- it silently changes what the user's declared type actually
// is, breaking any wrapper-specific method call.
// "boolean"->"bool" stays as a rename since Java's boolean really
// is a primitive (Boolean is the separate wrapper class, not
// handled by this list either, for the same reason).
// "String" is intentionally NOT renamed to "std::string" here --
// String is a real wrapper class (Processing.h) with Java-named
// methods (trim(), contains(), toLowerCase(), etc.) that
// std::string doesn't have. Renaming "String" to "std::string" as
// plain text would silently strip the sketch author's declared
// type down to the wrong class, exactly causing methods like
// .trim()/.contains() to fail to compile -- the wrapper class
// already inherits from std::string and is fully compatible with
// it, so there is no reason to rewrite the type name away at all.
{ "null", "nullptr" },
};
code = replaceKeywordsOutsideLiterals(code, words);
// List varname = ... -> std::vector varname = ...
code = code.replaceAll("\\bList<", "std::vector<");
// .add( -> .push_back( (vector method)
// Note: only when not preceded by a word char to avoid replacing e.g. "loadPixels"
// We keep .add() as push_back only for vector types - risky so skip for now
// âÂÂâ 2. Java cast syntax: float(x) â (float)(x) âÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂ
// Java cast syntax: float(x) â (float)(x)
// Exclude when inside template args (preceded by < or ,) to avoid
// mangling std::function into std::function<(bool)(...)>
for (String t : new String[]{ "float","int","double","long","char","bool" })
code = code.replaceAll("(?<![\\w<,\\s])(" + t + ")\\(", "($1)(");
// âÂÂâ 3. Hex color literals: #RRGGBB â color(0xFFRRGGBB) âÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂ
code = code.replaceAll("#([0-9A-Fa-f]{8})", "color(0x$1)");
code = code.replaceAll("#([0-9A-Fa-f]{6})", "color(0xFF$1)");
// âÂÂâ 4. String concatenation: "literal" + x â std::string("literal") + x âÂÂ
code = code.replaceAll("(\"[^\"]*\")\\s*\\+", "std::string($1) +");
// âÂÂâ 5. .charAt(i) â [i] âÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂ
code = code.replaceAll("\\.charAt\\((\\w+)\\)", "[$1]");
// âÂÂâ 6. .equals("...") â == "..." âÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂ
code = code.replaceAll("\\.equals\\(([^)]*)\\)", " == $1");
// âÂÂâ 6b. Wrap .length()/.size() in std::to_string() whenever they appear
// in a string concatenation context (adjacent to + operator).
// Run multiple passes to catch all patterns.
for (int _pass = 0; _pass < 3; _pass++) {
code = code.replaceAll("\\+\\s*(\\w+)\\.length\\(\\)", "+ std::to_string($1.length())");
code = code.replaceAll("(\\w+)\\.length\\(\\)\\s*\\+", "std::to_string($1.length()) +");
code = code.replaceAll("\\+\\s*(\\w+)\\.size\\(\\)", "+ std::to_string($1.size())");
code = code.replaceAll("(\\w+)\\.size\\(\\)\\s*\\+", "std::to_string($1.size()) +");
}
// âÂÂâ 7. float % int â (int)(float) % int (modulo on floats invalid in C++) âÂÂ
// BUG FIX, found by tracing a real compile failure (Sequential.pde,
// a real example sketch) all the way through every stage of the
// pipeline: the original regex, "\(([^)]*\.[^)]*)\)\s*%\s*(\w+)",
// matched ANY parenthesized text containing a literal dot character
// anywhere inside it, with no requirement that the dot be part of
// an actual FLOAT LITERAL -- a member-access dot (as in
// "images.get(") satisfies "[^)]*\.[^)]*" just as well as a real
// float literal like "0.05f" does. Confirmed precisely: this matched
// across "images.get((currentFrame + offset) % numFrames)" --
// treating the dot inside "images.get(" and the unrelated "%
// numFrames" much later in the SAME statement as if they were one
// float-modulo expression -- and rewrote "image(" itself into
// "image(int)(", which g++ then correctly rejected ("expected
// primary-expression before 'int'"). This was a genuine,
// PRE-EXISTING bug, confirmed byte-identical to the very first
// CppMode.zip uploaded at the start of this whole project -- never
// introduced by anything built during this project, only found now
// by tracing a real user-reported failure through every single
// stage of the real pipeline (parser, every AST pass, codegen, all
// confirmed correct via direct reflection against the real deployed
// jar) until this regex -- untouched since day one -- was the only
// remaining place left to check.
//
// Narrowed to require an actual float-LITERAL-shaped token
// (\d+\.\d+ or \.\d+, optionally with an f/F suffix) inside the
// parens, which is what this rule is actually meant to detect --
// confirmed against Objects.pde's real, legitimate, ALREADY-
// correctly-cast example ("(width*0.05f) % width") that this
// pattern is designed to help authors avoid forgetting -- not "any
// dot anywhere," which is true of any member access, scoped name,
// or qualified call and was never specific to floats at all.
code = code.replaceAll("\\(([^)]*\\d+\\.\\d+f?[^)]*|[^)]*\\.\\d+f?[^)]*)\\)\\s*%\\s*(\\w+)", "(int)($1) % $2");
// âÂÂâ 8. Windows reserved: far/near â farVal/nearVal (Windows only) âÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂ
if (System.getProperty("os.name").toLowerCase().contains("win")) {
code = code.replaceAll("\\bfar\\b", "farVal");
code = code.replaceAll("\\bnear\\b", "nearVal");
}
// (color type fixing handled by fixColorTypes() before javaToC())
// âÂÂâ 10. color() ambiguity: color(r,g,b,floatAlpha) â cast last arg âÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂ
code = code.replaceAll(
"\\bcolor\\(\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*(\\d+)\\s*,\\s*([^)\\d][^)]*)\\)",
"color($1,$2,$3,(int)($4))");
// âÂÂâ 11. API-conflicting variable names: scale/fill/stroke/etc. âÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂ
// If used as a variable (after a type keyword), rename to avoid shadowing API
String[] apiConflicts = {
"scale", "stroke", "background", "translate", "rotate",
"map", "dist", "noise"
};
String typeKw = "(?:int|float|double|bool|char|long|unsigned|auto|color|PVector)\\s+";
for (String word : apiConflicts) {
// Only rename if it appears as a variable declaration
if (java.util.regex.Pattern.compile(typeKw + word + "\\b(?!\\s*\\()", java.util.regex.Pattern.MULTILINE).matcher(code).find()) {
// Rename all bare occurrences (not followed by '(')
code = code.replaceAll("\\b" + word + "\\b(?!\\s*\\()", "_" + word);
}
}
// âÂÂâ 12. mousePressed / keyPressed as bool variables â _mousePressed / _keyPressed âÂÂ
// In Processing Java, 'mousePressed' is both a bool variable (read in draw)
// and an event callback method name (void mousePressed()).
// In C++ PApplet they can't share a name: the virtual method keeps the natural
// name, so occurrences used as a BOOL EXPRESSION (not followed by '(') must be
// rewritten to _mousePressed / _keyPressed.
// We use replaceKeywordsOutsideLiterals with a negative-lookahead-equivalent:
// replace \bmousePressed\b not followed by \s*( -- done via manual scan below.
code = rewriteBoolEventName(code, "mousePressed", "_mousePressed");
code = rewriteBoolEventName(code, "keyPressed", "_keyPressed");
// âÂÂâ 13. frameRate as a variable read â _frameRate âÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂ
// In Processing Java, 'frameRate' is both a setter function (frameRate(60))
// and a readable float variable (if (frameRate < 30)). In C++ PApplet, the
// method keeps the natural name. Variable reads (not followed by '(') must be
// rewritten to _frameRate, which _PSketch already exposes as a float proxy.
code = rewriteBoolEventName(code, "frameRate", "_frameRate");
return code;
}
// Replace all occurrences of `name` that are NOT immediately followed by '('
// (i.e. used as a variable, not as a function call or definition).
// Operates outside string/char literals and comments.
private String rewriteBoolEventName(String code, String name, String replacement) {
StringBuilder out = new StringBuilder();
int i = 0, n = code.length(), nlen = name.length();
while (i < n) {
char c = code.charAt(i);
// Skip line comments
if (c == '/' && i+1 < n && code.charAt(i+1) == '/') {
int end = code.indexOf('\n', i); if (end < 0) end = n;
out.append(code, i, end); i = end; continue;
}
// Skip block comments
if (c == '/' && i+1 < n && code.charAt(i+1) == '*') {
int end = code.indexOf("*/", i+2); end = end < 0 ? n : end+2;
out.append(code, i, end); i = end; continue;
}
// Skip string literals
if (c == '"') {
out.append(c); i++;
while (i < n) { char sc = code.charAt(i); out.append(sc); i++;
if (sc == '\\' && i < n) { out.append(code.charAt(i++)); continue; }
if (sc == '"') break; }
continue;
}
// Skip char literals
if (c == '\'') {
out.append(c); i++;
while (i < n) { char sc = code.charAt(i); out.append(sc); i++;
if (sc == '\\' && i < n) { out.append(code.charAt(i++)); continue; }
if (sc == '\'') break; }
continue;
}
// Check for name match
if (i + nlen <= n && code.substring(i, i+nlen).equals(name)) {
// Word boundary check: char before must not be letter/digit/_
boolean leftOk = i == 0 || (!Character.isLetterOrDigit(code.charAt(i-1)) && code.charAt(i-1) != '_');
// Char after must not be letter/digit/_
boolean rightOk = i+nlen >= n || (!Character.isLetterOrDigit(code.charAt(i+nlen)) && code.charAt(i+nlen) != '_');
if (leftOk && rightOk) {
// Check what follows (skipping whitespace): if it's '(' this is a call/def, don't replace
int j = i + nlen;
while (j < n && (code.charAt(j) == ' ' || code.charAt(j) == '\t')) j++;
boolean isCall = j < n && code.charAt(j) == '(';
if (isCall) {
out.append(code, i, i+nlen); i += nlen;
} else {
out.append(replacement); i += nlen;
}
continue;
}
}
out.append(c); i++;
}
return out.toString();
}
private List wordWrap(String line, int max) {
List r = new ArrayList<>();
while (line.length() > max) {
int s = line.lastIndexOf(' ', max);
if (s < 0) s = max;
r.add(line.substring(0, s));
line = " " + line.substring(s).stripLeading();
}
r.add(line);
return r;
}
private List buildCommand(String gpp, File src, File bin, boolean win, boolean mac) {
List cmd = new ArrayList<>();
cmd.add(gpp);
cmd.add("-std=c++20");
cmd.add("-O2");
// -march=native isn't reliably supported by Apple's clang (which is
// what g++/gcc resolve to on macOS via Xcode Command Line Tools) âÂÂ
// skip it there rather than risk a build failing on an unsupported flag.
if (!mac) cmd.add("-march=native");
// âÂÂâ Homebrew include/lib paths (macOS only) âÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂ
// Homebrew installs to /opt/homebrew on Apple Silicon and /usr/local on
// Intel Macs â neither is guaranteed to be on g++'s default search path,
// so add both explicitly. Harmless if a path doesn't exist.
String homebrewPrefix = null;
if (mac) {
if (new File("/opt/homebrew/include").exists()) homebrewPrefix = "/opt/homebrew";
else if (new File("/usr/local/include").exists()) homebrewPrefix = "/usr/local";
if (homebrewPrefix != null) {
cmd.add("-I" + homebrewPrefix + "/include");
}
}
// âÂÂâ Cached precompiled objects âÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂ
// Processing.cpp takes ~9s to compile. Cache the .o and reuse it.
try {
String osName = System.getProperty("os.name","").toLowerCase();
String cacheSubDir = osName.contains("win") ? "cache/windows-x64"
: osName.contains("mac") ? "cache/macos-" + (System.getProperty("os.arch","").contains("aarch64") ? "arm64" : "x64")
: "cache/linux-x64";
File cacheDir = new File(runtimeDir.getParentFile(), cacheSubDir);
cacheDir.mkdirs();
File processingO = new File(cacheDir, "Processing.o");
File defaultsO = new File(cacheDir, "Processing_defaults.o");
File processingCpp = new File(runtimeDir, "Processing.cpp");
File defaultsCpp = new File(runtimeDir, "Processing_defaults.cpp");
File processingH = new File(runtimeDir, "Processing.h");
boolean needsProcessing = !processingO.exists()
|| processingCpp.lastModified() > processingO.lastModified()
|| (processingH.exists() && processingH.lastModified() > processingO.lastModified());
boolean needsDefaults = !defaultsO.exists()
|| defaultsCpp.lastModified() > defaultsO.lastModified();
if (needsProcessing) {
List preCmd = new ArrayList<>();
preCmd.add(gpp); preCmd.add("-std=c++20"); preCmd.add("-O2");
if (!mac) preCmd.add("-march=native");
if (homebrewPrefix != null) preCmd.add("-I" + homebrewPrefix + "/include");
preCmd.add("-I" + runtimeDir.getAbsolutePath());
preCmd.add("-DPROCESSING_HAS_STB_IMAGE");
preCmd.add("-DPROCESSING_HAS_STB_TRUETYPE");
preCmd.add("-c"); preCmd.add(processingCpp.getAbsolutePath());
preCmd.add("-o"); preCmd.add(processingO.getAbsolutePath());
ProcessBuilder prePb = new ProcessBuilder(preCmd);
prePb.redirectErrorStream(true);
Process preP = prePb.start();
StringBuilder preLog = new StringBuilder();
try (java.io.BufferedReader br = new java.io.BufferedReader(
new java.io.InputStreamReader(preP.getInputStream()))) {
String line; while ((line=br.readLine())!=null) preLog.append(line).append('\n');
}
if (preP.waitFor() != 0 || !processingO.exists()) {
// Cache failed - fall back to inline compilation
cmd.add(processingCpp.getAbsolutePath());
} else {
}
}
if (needsDefaults) {
List preCmd2 = new ArrayList<>();
preCmd2.add(gpp); preCmd2.add("-std=c++20"); preCmd2.add("-O2");
if (!mac) preCmd2.add("-march=native");
if (homebrewPrefix != null) preCmd2.add("-I" + homebrewPrefix + "/include");
preCmd2.add("-I" + runtimeDir.getAbsolutePath());
preCmd2.add("-DPROCESSING_HAS_STB_IMAGE");
preCmd2.add("-DPROCESSING_HAS_STB_TRUETYPE");
preCmd2.add("-c"); preCmd2.add(defaultsCpp.getAbsolutePath());
preCmd2.add("-o"); preCmd2.add(defaultsO.getAbsolutePath());
ProcessBuilder prePb2 = new ProcessBuilder(preCmd2);
prePb2.redirectErrorStream(true);
if (prePb2.start().waitFor() != 0 || !defaultsO.exists()) {
cmd.add(defaultsCpp.getAbsolutePath());
}
}
// Add cached .o files if they exist, otherwise inline source
if (processingO.exists() && !needsProcessing)
cmd.add(processingO.getAbsolutePath());
else if (!cmd.contains(processingCpp.getAbsolutePath()))
cmd.add(processingCpp.getAbsolutePath());
if (defaultsO.exists() && !needsDefaults)
cmd.add(defaultsO.getAbsolutePath());
else if (!cmd.contains(defaultsCpp.getAbsolutePath()))
cmd.add(defaultsCpp.getAbsolutePath());
} catch (Exception _cacheEx) {
// Cache failed - fall back to compiling inline
System.err.println("[cache] error: " + _cacheEx.getMessage());
cmd.add(new File(runtimeDir, "Processing.cpp").getAbsolutePath());
cmd.add(new File(runtimeDir, "Processing_defaults.cpp").getAbsolutePath());
}
cmd.add(src.getAbsolutePath());
// main() is now emitted inside Sketch_run.cpp by writeSketch() -- don't link main.cpp
cmd.add("-I" + runtimeDir.getAbsolutePath());
cmd.add("-o"); cmd.add(bin.getAbsolutePath());
// Enable stb_image if present in mode's src/ folder
File stbImage = new File(runtimeDir, "stb_image.h");
if (stbImage.exists()) cmd.add("-DPROCESSING_HAS_STB_IMAGE");
File stbTTF = new File(runtimeDir, "stb_truetype.h");
if (stbTTF.exists()) cmd.add("-DPROCESSING_HAS_STB_TRUETYPE");
if (win) {
Collections.addAll(cmd,
"-lglfw3", "-lglew32", "-lopengl32", "-lglu32",
"-lcomdlg32", "-lshell32", "-lole32", "-luuid",
"-mwindows", "-pthread", "-D_USE_MATH_DEFINES");
} else if (mac) {
// macOS has no separate libGL/libGLU â OpenGL is a system framework,
// bundled with Xcode Command Line Tools (no extra install needed).
// GLFW/GLEW come from Homebrew, so link against its lib directory too.
if (homebrewPrefix != null) {
cmd.add("-L" + homebrewPrefix + "/lib");
}
Collections.addAll(cmd, "-lglfw", "-lGLEW", "-lm", "-pthread");
Collections.addAll(cmd, "-framework", "OpenGL");
Collections.addAll(cmd, "-framework", "Cocoa");
Collections.addAll(cmd, "-framework", "IOKit");
Collections.addAll(cmd, "-framework", "CoreVideo");
} else {
Collections.addAll(cmd, "-lglfw", "-lGLEW", "-lGL", "-lGLU", "-lm", "-pthread");
}
return cmd;
}
/**
* Run the sketch text through the real C/C++ preprocessor (g++ -E) so that
* all #define macros -- including multi-line X-macros with backslash
* continuations, token pasting (##), stringification (#), and conditional
* compilation (#ifdef) -- are fully expanded BEFORE any of CppBuild's own
* text-scanning passes (hoistClasses, removeHoistedClasses, enum/constexpr
* extraction, lifecycle-method rewriting) run.
*
* This is necessary because those passes are simple line/brace scanners
* that assume "what you see textually is what the C++ grammar sees." That
* assumption only holds true AFTER macro expansion -- a multi-line #define
* or an X-macro expanding into enum/switch bodies will otherwise corrupt
* the brace-depth tracking and produce garbage output.
*
* We preserve #line markers (no -P flag) so that g++'s own line-tracking
* stays roughly aligned; CppBuild re-stamps "#line 1 \"sketch.pde\"" right
* before emitting user code anyway, so exact line fidelity through the
* macro-expansion step is a secondary concern -- correctness of the
* expanded code is the primary goal.
*/
/** Strip "new TypeName(" only when TypeName matches the declared variable type. */
private static String stripMatchingNew(String code) {
// Match: BaseType<...> varName = new SameBase<...>(
// Capture group 1: base type name; group 2: full lhs; group 3: new type base
java.util.regex.Pattern p = java.util.regex.Pattern.compile(
"(\\w+)(?:<[^;={]*>)?\\s+\\w+\\s*=\\s*new\\s+(\\w+)(?:<[^;={]*>)?\\s*\\(");
java.util.regex.Matcher m = p.matcher(code);
StringBuffer sb = new StringBuffer();
while (m.find()) {
String declBase = m.group(1);
String newBase = m.group(2);
if (declBase.equals(newBase)) {
// Same base type: strip "new "
String replaced = m.group(0).replaceFirst("new\\s+", "");
m.appendReplacement(sb, java.util.regex.Matcher.quoteReplacement(replaced));
} else {
m.appendReplacement(sb, java.util.regex.Matcher.quoteReplacement(m.group(0)));
}
}
m.appendTail(sb);
return sb.toString();
}
private String preprocessMacros(String code) {
// Fast path: if there's no #define/#ifdef/#undef/#include anywhere,
// skip invoking an external process entirely.
if (!code.contains("#define") && !code.contains("#ifdef")
&& !code.contains("#ifndef") && !code.contains("#undef")
&& !code.contains("#if ")) {
return code;
}
try {
// Strip #include lines before preprocessing -- we only want macro
// expansion (#define/#ifdef), not system header inclusion which
// causes g++ -E to interleave sketch code with STL internals.
StringBuilder noIncludes = new StringBuilder();
for (String line : code.split("\n", -1)) {
String t = line.strip();
if (t.startsWith("#include")) {
noIncludes.append("\n"); // preserve line numbers
} else {
noIncludes.append(line).append("\n");
}
}
File scratch = new File(buildDir, "_macro_preprocess_input.cpp");
Files.writeString(scratch.toPath(), noIncludes.toString());
boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win");
String gpp = findGpp(isWindows);
// -E: stop after preprocessing. -x c++: force C++ mode regardless of
// the .cpp extension's default. -P would strip line markers; we keep
// them (omit -P) since downstream code already re-stamps its own
// #line directive, and keeping g++'s markers costs nothing here.
ProcessBuilder pb = new ProcessBuilder(gpp, "-E", "-x", "c++", scratch.getAbsolutePath());
pb.directory(buildDir);
pb.redirectErrorStream(false);
Process proc = pb.start();
String expanded = new String(proc.getInputStream().readAllBytes());
String errOutput = new String(proc.getErrorStream().readAllBytes());
int exitCode = proc.waitFor();
if (exitCode != 0 || expanded.isBlank()) {
// Preprocessing failed (e.g. malformed macro) -- fall back to the
// original, unexpanded code rather than silently producing nothing.
// The downstream compile step will surface a clearer error in that
// case, and we don't want a preprocessing hiccup to mask the
// sketch entirely.
System.err.println("[CppMode] macro preprocessing failed, using original code: " + errOutput);
return code;
}
// g++ -E emits "# \"\" " GNU-style line markers.
// We keep ONLY lines that originate from the scratch file itself,
// filtering out system header expansions (#include etc.)
// which would overwhelm the CppMode parser with STL internals.
String scratchPath = scratch.getAbsolutePath();
StringBuilder cleaned = new StringBuilder();
boolean inScratchFile = true; // start in sketch file
for (String line : expanded.split("\n", -1)) {
String t = line.strip();
// GNU line marker: "# N \"filename\" flags"
if (t.startsWith("# ") && t.length() > 2 && Character.isDigit(t.charAt(2))) {
// System headers have /usr/, /lib/, or c++/ in path
boolean isSystemHeader = t.contains("/usr/") || t.contains("/lib/") || t.contains("c++/");
// Scratch file or built-ins
boolean isScratch = t.contains(scratchPath) || t.contains("") || t.contains("") || t.contains("");
if (isSystemHeader) inScratchFile = false;
else if (isScratch) inScratchFile = true;
// flag=2 means "returned to file after include" -- always re-enable for scratch
else inScratchFile = true; // unknown file -- keep (could be user header)
continue; // never emit line markers themselves
}
if (t.startsWith("#line")) continue;
if (t.startsWith("#pragma")) continue; // strip pragmas from system headers
if (!inScratchFile) continue; // skip system header content
cleaned.append(line).append("\n");
}
return cleaned.toString();
} catch (Exception e) {
System.err.println("[CppMode] macro preprocessing exception, using original code: " + e);
return code;
}
}
private String findGpp(boolean win) {
if (win) {
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()) return p;
}
return "g++";
}
// âÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂ
// EXPORT APPLICATION SYSTEM
// âÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂ
public enum ExportTarget {
LINUX_X64 ("linux-x64", "g++", "Linux 64-bit (x86_64)", false),
LINUX_ARM64 ("linux-arm64", "aarch64-linux-gnu-g++", "Linux ARM64 (RPi 4/5, 64-bit)", false),
LINUX_ARM32 ("linux-arm32", "arm-linux-gnueabihf-g++", "Linux ARM32 (RPi 2/3, 32-bit)", false),
WINDOWS_X64 ("windows-x64", "x86_64-w64-mingw32-g++", "Windows 64-bit", true),
MACOS_X64 ("macos-x64", "x86_64-apple-darwin23-c++", "macOS Intel 64-bit", false),
MACOS_ARM64 ("macos-arm64", "aarch64-apple-darwin23-c++", "macOS Apple Silicon", false);
public final String dirName, compiler, label;
public final boolean isWindows;
ExportTarget(String d, String c, String l, boolean w) {
dirName=d; compiler=c; label=l; isWindows=w;
}
}
public void showExportDialog(Sketch sketch, RunnerListener listener) {
java.util.Map avail = new java.util.LinkedHashMap<>();
for (ExportTarget t : ExportTarget.values()) avail.put(t, compilerExists(t));
String os = System.getProperty("os.name","").toLowerCase();
boolean isWin = os.contains("win");
java.awt.Color bg = new java.awt.Color(28,30,38);
java.awt.Color bgRow = new java.awt.Color(35,37,48);
java.awt.Color green = new java.awt.Color(100,220,120);
java.awt.Color red = new java.awt.Color(240,80,80);
java.awt.Color blue = new java.awt.Color(210,228,255);
javax.swing.JDialog dlg = new javax.swing.JDialog((java.awt.Frame)null,"Export Application",true);
dlg.setLayout(new java.awt.BorderLayout(8,8));
dlg.getContentPane().setBackground(bg);
// âÂÂâ Title âÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂ
javax.swing.JLabel title = new javax.swing.JLabel(" Export Application");
title.setForeground(blue);
title.setFont(title.getFont().deriveFont(java.awt.Font.BOLD,15f));
title.setBorder(javax.swing.BorderFactory.createEmptyBorder(14,8,4,8));
dlg.add(title, java.awt.BorderLayout.NORTH);
javax.swing.JPanel center = new javax.swing.JPanel(new java.awt.BorderLayout(4,6));
center.setBackground(bg);
center.setBorder(javax.swing.BorderFactory.createEmptyBorder(0,14,4,14));
// âÂÂâ Icon picker âÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂ
javax.swing.JPanel iconPanel = new javax.swing.JPanel(new java.awt.BorderLayout(6,4));
iconPanel.setBackground(bg);
iconPanel.setBorder(javax.swing.BorderFactory.createEmptyBorder(4,0,8,0));
// Top row: label + hint
javax.swing.JPanel iconTop = new javax.swing.JPanel(new java.awt.FlowLayout(java.awt.FlowLayout.LEFT,4,0));
iconTop.setBackground(bg);
javax.swing.JLabel iconLbl = new javax.swing.JLabel("App Icon (PNG) ");
iconLbl.setForeground(blue);
iconLbl.setFont(iconLbl.getFont().deriveFont(java.awt.Font.BOLD, 12f));
javax.swing.JLabel iconHint = new javax.swing.JLabel("optional ÷ 256ÃÂ256 recommended ÷ leave blank to skip");
iconHint.setForeground(new java.awt.Color(110,115,150));
iconHint.setFont(iconHint.getFont().deriveFont(10f));
iconTop.add(iconLbl); iconTop.add(iconHint);
iconPanel.add(iconTop, java.awt.BorderLayout.NORTH);
// Bottom row: preview + path field + buttons
javax.swing.JPanel iconBottom = new javax.swing.JPanel(new java.awt.BorderLayout(6,0));
iconBottom.setBackground(bg);
// Icon preview label (shows thumbnail)
javax.swing.JLabel iconPreview = new javax.swing.JLabel();
iconPreview.setPreferredSize(new java.awt.Dimension(48,48));
iconPreview.setOpaque(true);
iconPreview.setBackground(new java.awt.Color(40,42,55));
iconPreview.setBorder(javax.swing.BorderFactory.createLineBorder(new java.awt.Color(60,65,90)));
iconPreview.setHorizontalAlignment(javax.swing.JLabel.CENTER);
iconBottom.add(iconPreview, java.awt.BorderLayout.WEST);
// Path field - read only, updated by file chooser
javax.swing.JTextField iconField = new javax.swing.JTextField();
iconField.setBackground(new java.awt.Color(40,42,55));
iconField.setForeground(blue); iconField.setCaretColor(blue);
iconField.setBorder(javax.swing.BorderFactory.createCompoundBorder(
javax.swing.BorderFactory.createLineBorder(new java.awt.Color(60,65,90)),
javax.swing.BorderFactory.createEmptyBorder(2,6,2,6)));
iconField.setFont(iconField.getFont().deriveFont(11f));
// Helper to update preview
Runnable[] updatePreview = {null};
updatePreview[0] = () -> {
String p = iconField.getText().trim();
if (!p.isEmpty() && new File(p).exists()) {
try {
java.awt.image.BufferedImage img = javax.imageio.ImageIO.read(new File(p));
if (img != null) {
java.awt.Image scaled = img.getScaledInstance(44, 44, java.awt.Image.SCALE_SMOOTH);
iconPreview.setIcon(new javax.swing.ImageIcon(scaled));
iconPreview.setText("");
}
} catch (Exception ignored) { iconPreview.setIcon(null); iconPreview.setText("?"); }
} else {
iconPreview.setIcon(null);
iconPreview.setText(p.isEmpty() ? "none" : "!");
iconPreview.setForeground(new java.awt.Color(120,125,155));
}
};
// Pre-fill if sketch has icon.png
for (String p : new String[]{"icon.png","data/icon.png"}) {
File ic = new File(sketch.getFolder(), p);
if (ic.exists()) { iconField.setText(ic.getAbsolutePath()); break; }
}
updatePreview[0].run();
// Buttons panel
javax.swing.JPanel iconBtns = new javax.swing.JPanel(new java.awt.GridLayout(2,1,2,2));
iconBtns.setBackground(bg);
javax.swing.JButton iconBrowse = new javax.swing.JButton("Browse...");
iconBrowse.setFont(iconBrowse.getFont().deriveFont(11f));
iconBrowse.addActionListener(e -> {
javax.swing.JFileChooser fc = new javax.swing.JFileChooser(
iconField.getText().isEmpty() ? sketch.getFolder().getAbsolutePath()
: new File(iconField.getText()).getParent());
fc.setDialogTitle("Select App Icon (PNG)");
fc.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("PNG Images (*.png)","png"));
fc.setFileSelectionMode(javax.swing.JFileChooser.FILES_ONLY);
fc.setPreferredSize(new java.awt.Dimension(700, 500));
if (fc.showOpenDialog(dlg) == javax.swing.JFileChooser.APPROVE_OPTION) {
iconField.setText(fc.getSelectedFile().getAbsolutePath());
updatePreview[0].run();
}
});
javax.swing.JButton iconClear = new javax.swing.JButton("Clear");
iconClear.setFont(iconClear.getFont().deriveFont(11f));
iconClear.setForeground(new java.awt.Color(180,80,80));
iconClear.addActionListener(e -> {
iconField.setText("");
updatePreview[0].run();
});
iconBtns.add(iconBrowse); iconBtns.add(iconClear);
iconBottom.add(iconField, java.awt.BorderLayout.CENTER);
iconBottom.add(iconBtns, java.awt.BorderLayout.EAST);
iconPanel.add(iconBottom, java.awt.BorderLayout.CENTER);
center.add(iconPanel, java.awt.BorderLayout.NORTH);
// âÂÂâ Target checkboxes âÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂ
javax.swing.JPanel targets = new javax.swing.JPanel();
targets.setLayout(new javax.swing.BoxLayout(targets, javax.swing.BoxLayout.Y_AXIS));
targets.setBackground(bg);
java.util.Map boxes = new java.util.LinkedHashMap<>();
for (ExportTarget t : ExportTarget.values()) {
// Show all targets everywhere - unavailable ones shown grayed out
boolean ok = avail.getOrDefault(t, false);
boolean isMac = t==ExportTarget.MACOS_X64 || t==ExportTarget.MACOS_ARM64;
javax.swing.JPanel row = new javax.swing.JPanel(new java.awt.FlowLayout(java.awt.FlowLayout.LEFT,5,2));
row.setBackground(bgRow);
row.setMaximumSize(new java.awt.Dimension(Integer.MAX_VALUE, 34));
javax.swing.JCheckBox cb = new javax.swing.JCheckBox(t.label);
cb.setBackground(bgRow); cb.setEnabled(ok);
cb.setForeground(ok ? blue : red);
if (!ok) cb.setFont(cb.getFont().deriveFont(java.awt.Font.ITALIC));
cb.setSelected(ok && (
(isWin && t==ExportTarget.WINDOWS_X64) ||
(!isWin && (t==ExportTarget.LINUX_X64 || t==ExportTarget.WINDOWS_X64))));
if (isMac && ok) cb.addActionListener(e -> {
if (cb.isSelected()) javax.swing.JOptionPane.showMessageDialog(dlg,
"macOS export requires osxcross on your PATH.
"+
"This may take several minutes to compile.
"+
"If osxcross is not installed, export will fail.",
"macOS Warning", javax.swing.JOptionPane.WARNING_MESSAGE);
});
row.add(cb);
boxes.put(t, cb);
javax.swing.JLabel statusLbl = new javax.swing.JLabel(ok ? "âÂÂ" : "â not found");
statusLbl.setForeground(ok ? green : red);
statusLbl.setFont(statusLbl.getFont().deriveFont(10f));
row.add(statusLbl);
if (!ok) {
javax.swing.JButton installBtn = new javax.swing.JButton("Install");
installBtn.setBackground(new java.awt.Color(50,80,150));
installBtn.setForeground(java.awt.Color.WHITE);
installBtn.setFont(installBtn.getFont().deriveFont(10f));
installBtn.setMargin(new java.awt.Insets(1,6,1,6));
final ExportTarget ft=t; final javax.swing.JCheckBox fcb=cb;
final javax.swing.JLabel fsl=statusLbl;
installBtn.addActionListener(e -> {
installBtn.setText("Installing...");
installBtn.setEnabled(false);
new Thread(() -> {
boolean ok2 = installCompilerForTarget(ft, listener);
javax.swing.SwingUtilities.invokeLater(() -> {
if (ok2 && compilerExists(ft)) {
fcb.setEnabled(true); fcb.setForeground(blue);
fcb.setFont(fcb.getFont().deriveFont(java.awt.Font.PLAIN));
fsl.setText("â ready"); fsl.setForeground(green);
installBtn.setText("â Done");
installBtn.setBackground(new java.awt.Color(40,110,55));
} else {
installBtn.setText("Retry"); installBtn.setEnabled(true);
fsl.setText("â failed");
}
});
}).start();
});
row.add(installBtn);
String hint = getInstallHint(t);
if (!hint.isEmpty()) {
javax.swing.JLabel hl = new javax.swing.JLabel(" " + hint);
hl.setForeground(new java.awt.Color(110,115,145));
hl.setFont(hl.getFont().deriveFont(9f));
row.add(hl);
}
}
targets.add(row);
targets.add(javax.swing.Box.createVerticalStrut(2));
}
javax.swing.JScrollPane scroll = new javax.swing.JScrollPane(targets);
scroll.setBorder(null); scroll.getViewport().setBackground(bg);
center.add(scroll, java.awt.BorderLayout.CENTER);
dlg.add(center, java.awt.BorderLayout.CENTER);
// âÂÂâ Buttons âÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂâÂÂ
javax.swing.JPanel btnRow = new javax.swing.JPanel(new java.awt.FlowLayout(java.awt.FlowLayout.RIGHT,10,10));
btnRow.setBackground(bg);
javax.swing.JButton cancel = new javax.swing.JButton("Cancel");
javax.swing.JButton export = new javax.swing.JButton("Export");
export.setBackground(new java.awt.Color(55,95,175)); export.setForeground(java.awt.Color.WHITE);
boolean[] go = {false};
cancel.addActionListener(e -> dlg.dispose());
export.addActionListener(e -> { go[0]=true; dlg.dispose(); });
btnRow.add(cancel); btnRow.add(export);
dlg.add(btnRow, java.awt.BorderLayout.SOUTH);
dlg.pack();
dlg.setMinimumSize(new java.awt.Dimension(560, 380));
dlg.setLocationRelativeTo(null);
dlg.setVisible(true);
if (!go[0]) return;
java.util.List selected = new java.util.ArrayList<>();
for (java.util.Map.Entry e : boxes.entrySet())
if (e.getValue().isSelected()) selected.add(e.getKey());
if (selected.isEmpty()) return;
final String iconPath = iconField.getText().trim();
new Thread(() -> runExport(sketch, listener, selected, iconPath)).start();
}
private String getInstallHint(ExportTarget t) {
String os = System.getProperty("os.name","").toLowerCase();
boolean isWin = os.contains("win");
if (isWin) {
switch (t) {
case WINDOWS_X64: return "opens MSYS2 installer";
case LINUX_X64: case LINUX_ARM64: case LINUX_ARM32:
return "requires Linux - export from a Linux machine";
case MACOS_X64: case MACOS_ARM64: return "opens osxcross guide";
default: return "";
}
}
switch (t) {
case WINDOWS_X64: return isWin ? "opens MSYS2 installer" : "pacman -S mingw-w64-gcc";
case LINUX_X64: return "downloads ~50MB toolchain";
case LINUX_ARM64: return "downloads ~100MB toolchain";
case LINUX_ARM32: return "downloads ~100MB toolchain";
case MACOS_X64: case MACOS_ARM64: return "opens osxcross guide";
default: return "";
}
}
private boolean installCompilerForTarget(ExportTarget t, RunnerListener listener) {
String os = System.getProperty("os.name","").toLowerCase();
boolean isWin = os.contains("win");
try {
String cmd = null;
switch (t) {
case WINDOWS_X64:
if (isWin) {
// On Windows, need MSYS2
java.awt.Desktop.getDesktop().browse(new java.net.URI("https://www.msys2.org"));
javax.swing.JOptionPane.showMessageDialog(null,
"Opening MSYS2 download page.
" +
"After installing MSYS2, open the MSYS2 terminal and run:
" +
"pacman -S mingw-w64-x86_64-gcc mingw-w64-x86_64-glfw mingw-w64-x86_64-glew
" +
"Then restart Processing and try again.",
"Install MSYS2", javax.swing.JOptionPane.INFORMATION_MESSAGE);
return false;
}
cmd = cmdExists("yay")
? "yay -S --noconfirm --needed mingw-w64-gcc mingw-w64-glfw mingw-w64-glew"
: "pkexec pacman -S --noconfirm --needed mingw-w64-gcc mingw-w64-glfw mingw-w64-glew";
break;
case LINUX_X64:
case LINUX_ARM64:
case LINUX_ARM32:
if (isWin) {
javax.swing.JOptionPane.showMessageDialog(null,
"Linux export not available on Windows.
" +
"C++ must be compiled natively for each platform.
" +
"To build Linux exports:
" +
" ⢠Open this sketch on a Linux machine
" +
" ⢠Use File â Export Application there
" +
"The exported Linux AppImage will run on any Linux distro.",
"Linux Export", javax.swing.JOptionPane.INFORMATION_MESSAGE);
return false;
}
// On Linux/Mac: download musl toolchain
return downloadMuslToolchain(t, listener);
case MACOS_X64: case MACOS_ARM64:
java.awt.Desktop.getDesktop().browse(
new java.net.URI("https://github.com/tpoechtrager/osxcross#installation"));
javax.swing.JOptionPane.showMessageDialog(null,
"Opening osxcross setup guide.
" +
"Follow the instructions to install osxcross,
" +
"then add it to your PATH and restart Processing.",
"macOS Cross-Compiler", javax.swing.JOptionPane.INFORMATION_MESSAGE);
return false;
default: return false;
}
if (cmd != null) {
listener.statusNotice("Installing " + t.label + " compiler...");
ProcessBuilder pb = isWin
? new ProcessBuilder("cmd", "/c", cmd)
: new ProcessBuilder("bash", "-c", cmd);
pb.redirectErrorStream(true);
Process p = pb.start();
StringBuilder out = new StringBuilder();
try (java.io.BufferedReader br = new java.io.BufferedReader(
new java.io.InputStreamReader(p.getInputStream()))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println("[install] " + line);
out.append(line).append('\n');
}
}
int ret = p.waitFor();
if (ret != 0) {
String log = out.toString();
String snippet = log.length() > 800 ? log.substring(log.length()-800) : log;
javax.swing.SwingUtilities.invokeLater(() ->
javax.swing.JOptionPane.showMessageDialog(null,
"Install failed (exit " + ret + ")
" +
"" + snippet.replace("<","<").replace(">",">") + "",
"Install Failed", javax.swing.JOptionPane.ERROR_MESSAGE));
return false;
}
return true;
}
} catch (Exception e) {
System.err.println("Install failed: " + e.getMessage());
javax.swing.JOptionPane.showMessageDialog(null,
"Install failed: " + e.getMessage() + "",
"Install Error", javax.swing.JOptionPane.ERROR_MESSAGE);
}
return false;
}
private void runExport(Sketch sketch, RunnerListener listener,
java.util.List targets, String iconPath) {
listener.statusNotice("Exporting...");
String skName = sketch.getName();
File outBase = new File(sketch.getFolder().getParentFile(), skName + "-export");
outBase.mkdirs();
String modeSrc = runtimeDir.getParentFile().getAbsolutePath() + "/src";
// Use writeSketch() to preprocess the sketch into Sketch_run.cpp
java.util.List sources = new java.util.ArrayList<>();
File exportBuildDir = sketch.makeTempFolder();
try {
// writeSketch produces Sketch_run.cpp with all preprocessing done
CppBuild helper = new CppBuild(sketch, mode);
helper.writeSketch(listener);
File sketchCpp = new File(helper.buildDir, "Sketch_run.cpp");
if (sketchCpp.exists()) {
sources.add(sketchCpp);
} else {
throw new Exception("Sketch_run.cpp not found after writeSketch");
}
} catch (Exception ex) {
// Fallback: copy .pde as .cpp
for (processing.app.SketchCode sc : sketch.getCode()) {
File f = sc.getFile();
if (f != null && f.exists()) {
String name = f.getName().replaceAll("\\.pde$", ".cpp");
File cpp = new File(exportBuildDir, name);
try {
java.nio.file.Files.copy(f.toPath(), cpp.toPath(),
java.nio.file.StandardCopyOption.REPLACE_EXISTING);
sources.add(cpp);
} catch (Exception ignored) {}
}
}
}
if (sources.isEmpty()) { listener.statusError("No source files"); return; }
// Auto-install missing dependencies for each target before compiling
installExportDeps(targets, listener);
int done = 0;
java.util.List results = new java.util.ArrayList<>();
for (ExportTarget t : targets) {
listener.statusNotice("Exporting " + t.label + "...");
try {
exportTarget(sketch, t, outBase, modeSrc, skName, sources, listener, iconPath);
results.add("\u2713 " + t.label); done++;
} catch (Exception ex) {
results.add("\u2717 " + t.label + ": " + ex.getMessage());
System.err.println("Export failed " + t.label + ": " + ex.getMessage());
}
}
final int d = done, tot = targets.size();
final String sum = String.join("
", results);
final String path = outBase.getAbsolutePath();
listener.statusNotice(d + "/" + tot + " targets exported");
javax.swing.SwingUtilities.invokeLater(() -> {
javax.swing.JOptionPane.showMessageDialog(null,
"Export: " + d + "/" + tot + " targets
"
+ sum + "
Output: " + path + "",
"Export Result", d>0
? javax.swing.JOptionPane.INFORMATION_MESSAGE
: javax.swing.JOptionPane.ERROR_MESSAGE);
if (d > 0) {
try { new ProcessBuilder("xdg-open", path).start(); }
catch (Exception ignored) {}
}
});
}
private void exportTarget(Sketch sketch, ExportTarget t,
File outBase, String modeSrc, String skName,
java.util.List sources,
RunnerListener listener, String iconPath) throws Exception {
File dir = new File(outBase, t.dirName); dir.mkdirs();
String binName = skName + (t.isWindows ? ".exe" : "");
File binOut = new File(dir, binName);
java.util.List cmd = new java.util.ArrayList<>();
String os = System.getProperty("os.name","").toLowerCase();
boolean onWin = os.contains("win");
if (onWin && t == ExportTarget.WINDOWS_X64) {
// Native Windows build - find MSYS2 g++
String compiler = "g++";
String[] msysPaths = {
"C:\\msys64\\mingw64\\bin\\g++.exe",
"C:\\msys2\\mingw64\\bin\\g++.exe",
System.getProperty("user.home") + "\\msys64\\mingw64\\bin\\g++.exe"
};
for (String p : msysPaths)
if (new File(p).exists()) { compiler = p; break; }
cmd.add(compiler);
} else if (!t.isWindows && t != ExportTarget.MACOS_X64
&& t != ExportTarget.MACOS_ARM64) {
// Linux targets: use bundled musl toolchain if available
File muslComp = getMuslCompiler(t);
if (muslComp != null) {
cmd.add(muslComp.getAbsolutePath());
} else {
cmd.add(t.compiler); // fall back to system compiler
}
} else {
cmd.add(t.compiler);
}
cmd.add("-std=c++20"); cmd.add("-O2");
for (File s : sources) cmd.add(s.getAbsolutePath());
// Use pre-built cached .o if available for this target
String exportCacheDir = t.isWindows ? "cache/windows-x64"
: (t == ExportTarget.LINUX_ARM64 ? "cache/linux-arm64"
: (t == ExportTarget.LINUX_ARM32 ? "cache/linux-arm32"
: "cache/linux-x64"));
File exportCache = new File(runtimeDir.getParentFile(), exportCacheDir);
File cachedProcessingO = new File(exportCache, "Processing.o");
File cachedDefaultsO = new File(exportCache, "Processing_defaults.o");
if (cachedProcessingO.exists()) {
cmd.add(cachedProcessingO.getAbsolutePath());
} else {
cmd.add(modeSrc + "/Processing.cpp");
}
if (cachedDefaultsO.exists()) {
cmd.add(cachedDefaultsO.getAbsolutePath());
} else {
cmd.add(modeSrc + "/Processing_defaults.cpp");
}
cmd.add("-I" + modeSrc);
cmd.add("-DPROCESSING_HAS_STB_IMAGE");
// Debug mode (and the website base URL used in error messages) are
// both controlled by config/cppmode.properties, a single
// consolidated file at the root of the CppMode folder (a sibling of
// src/). Shipped builds always have debug=0. To see PDEBUG(...)
// output, edit that file to set debug=1 and re-run the sketch --
// no environment variables to remember, no rebuilding CppBuild
// itself, works the same regardless of which terminal (or none at
// all) launched the IDE. This is the SAME file scripts/rebuild-
// engine.sh reads for the standalone engine-rebuild path, so both
// ways of building stay in sync automatically.
java.io.File configFile = new java.io.File(runtimeDir.getParentFile(), "config/cppmode.properties");
if (configFile.exists()) {
try {
java.util.Properties props = new java.util.Properties();
try (java.io.InputStream in = new java.io.FileInputStream(configFile)) {
props.load(in);
}
if ("1".equals(props.getProperty("debug", "0").trim())) {
cmd.add("-DPROCESSING_DEBUG");
}
String websiteUrl = props.getProperty("website.base.url");
if (websiteUrl != null && !websiteUrl.trim().isEmpty()) {
cmd.add("-DPROCESSING_WEBSITE_URL=\"" + websiteUrl.trim() + "\"");
}
} catch (java.io.IOException e) {
// If the config file can't be read for some reason, just
// proceed with a normal, non-debug build rather than failing
// the compile entirely over a missing diagnostic toggle.
}
}
cmd.add("-DPROCESSING_EXPORT");
// main() is now emitted inside Sketch_run.cpp -- don't link main.cpp separately
// For Windows: pre-compile icon resource and add to link
if (t.isWindows && iconPath != null && !iconPath.isEmpty()
&& new File(iconPath).exists()) {
File resFile = compileIconResource(iconPath, dir, t);
if (resFile != null) cmd.add(resFile.getAbsolutePath());
}
if (t.isWindows) addWinFlags(cmd);
else if (t==ExportTarget.MACOS_X64 || t==ExportTarget.MACOS_ARM64) addMacFlags(cmd);
else addLinuxFlags(cmd);
cmd.add("-o"); cmd.add(binOut.getAbsolutePath());
System.out.println("$ " + String.join(" ", cmd));
ProcessBuilder pb = new ProcessBuilder(cmd);
pb.redirectErrorStream(true);
pb.directory(sketch.getFolder());
Process p = pb.start();
StringBuilder log = new StringBuilder();
try (java.io.BufferedReader br = new java.io.BufferedReader(
new java.io.InputStreamReader(p.getInputStream()))) {
String line; while ((line=br.readLine())!=null) { System.out.println(line); log.append(line).append("\n"); }
}
if (p.waitFor() != 0) throw new Exception(log.substring(0, Math.min(400, log.length())));
// Post-build
if (t.isWindows) { mkBat(dir, binName); } // no DLLs needed - statically linked
else if (t==ExportTarget.MACOS_X64 || t==ExportTarget.MACOS_ARM64) mkAppBundle(dir, skName, binOut, iconPath);
else {
// Try to produce an AppImage (single-file, shows icon in Dolphin)
boolean madeAppImage = mkAppImage(dir, binName, skName, iconPath, listener);
if (!madeAppImage) mkSh(dir, binName); // fallback to run.sh
}
// Copy data/
File data = new File(sketch.getFolder(), "data");
if (data.exists()) copyDir(data, new File(dir, "data"));
// Copy font
File fontSrc = new File(runtimeDir.getParentFile(), "fonts/ProcessingSansPro-Regular.ttf");
if (fontSrc.exists()) copyFile(fontSrc, new File(dir, "ProcessingSansPro-Regular.ttf"));
}
private void addLinuxFlags(java.util.List cmd) {
// Use static GLFW/GLEW if available, otherwise dynamic
boolean staticGlfw = new java.io.File("/usr/lib/libglfw3.a").exists()
|| new java.io.File("/usr/local/lib/libglfw3.a").exists();
boolean staticGlew = new java.io.File("/usr/lib/libGLEW.a").exists()
|| new java.io.File("/usr/local/lib/libGLEW.a").exists();
if (staticGlfw && staticGlew) {
cmd.add("-Wl,-Bstatic"); cmd.add("-lglfw3"); cmd.add("-lGLEW");
cmd.add("-Wl,-Bdynamic");
} else {
cmd.add("-lglfw"); cmd.add("-lGLEW");
}
cmd.add("-lGL"); cmd.add("-lm"); cmd.add("-ldl"); cmd.add("-lpthread");
cmd.add("-lX11"); cmd.add("-lXrandr"); cmd.add("-lXi");
cmd.add("-lXxf86vm"); cmd.add("-lXinerama"); cmd.add("-lXcursor");
cmd.add("-static-libgcc"); cmd.add("-static-libstdc++");
}
private void addWinFlags(java.util.List cmd) {
String os = System.getProperty("os.name","").toLowerCase();
String[] roots;
if (os.contains("win")) {
// On Windows, use MSYS2 mingw64 paths
roots = new String[]{
"C:\\msys64\\mingw64",
"C:\\msys2\\mingw64",
System.getProperty("user.home") + "\\msys64\\mingw64"
};
} else {
// On Linux, use cross-compile sysroot
roots = new String[]{
"/usr/x86_64-w64-mingw32",
"/usr/local/x86_64-w64-mingw32",
"/opt/mingw-w64/x86_64-w64-mingw32"
};
}
String root = roots[0];
for (String r : roots) if (new File(r+(os.contains("win")?"\\lib":"/lib")).exists()) { root=r; break; }
String sep = os.contains("win") ? "\\" : "/";
// Include paths - add all that exist
for (String inc : new String[]{root+sep+"include", root+sep+"include"+sep+"GL"})
if (new File(inc).exists()) cmd.add("-I"+inc);
// Lib path
cmd.add("-L"+root+sep+"lib");
// Also try bin dir for DLL import libs
if (new File(root+sep+"bin").exists()) cmd.add("-L"+root+sep+"bin");
// Link libraries - DLLs shipped alongside exe
cmd.add("-lglfw3");
cmd.add("-lglew32");
cmd.add("-lopengl32");
cmd.add("-lgdi32");
cmd.add("-luser32");
cmd.add("-lshell32");
cmd.add("-lwinmm");
cmd.add("-lole32");
cmd.add("-luuid");
cmd.add("-mwindows"); // WinMain = no console terminal
cmd.add("-static-libgcc");
cmd.add("-static-libstdc++");
}
private void addMacFlags(java.util.List cmd) {
String[] roots = {
System.getProperty("user.home")+"/osxcross/target/SDK/MacOSX14.0.sdk",
"/opt/osxcross/target/SDK/MacOSX14.0.sdk"
};
for (String r : roots) if (new File(r).exists()) { cmd.add("--sysroot="+r); break; }
cmd.add("-framework"); cmd.add("OpenGL");
cmd.add("-framework"); cmd.add("Cocoa");
cmd.add("-framework"); cmd.add("IOKit");
cmd.add("-framework"); cmd.add("CoreVideo");
cmd.add("-lglfw"); cmd.add("-lGLEW");
cmd.add("-mmacosx-version-min=11.0");
}
private void copyWinDlls(File dir) {
String[] dlls = {"glfw3.dll","glew32.dll","libgcc_s_seh-1.dll",
"libstdc++-6.dll","libwinpthread-1.dll"};
// Bundled DLLs ship with CppMode - use these first (always available)
File bundledLibs = new File(runtimeDir.getParentFile(), "libs/windows-x64");
// Fallback: system mingw installation
String[] searchRoots = {
bundledLibs.getAbsolutePath(),
"/usr/x86_64-w64-mingw32/bin",
"/usr/x86_64-w64-mingw32/lib",
"/usr/lib/gcc/x86_64-w64-mingw32"
};
for (String dll : dlls) {
File found = findFile(dll, searchRoots);
if (found != null) {
try { copyFile(found, new File(dir, dll)); System.out.println("[dll] copied " + dll); }
catch (Exception e) { System.err.println("[dll] failed " + dll + ": " + e.getMessage()); }
} else {
System.err.println("[dll] WARNING: not found: " + dll);
}
}
}
private File findFile(String name, String[] roots) {
for (String root : roots) {
File f = new File(root, name);
if (f.exists()) return f;
// Search one level deep
File dir = new File(root);
if (dir.exists() && dir.listFiles() != null) {
for (File sub : dir.listFiles()) {
if (sub.isDirectory()) {
File f2 = new File(sub, name);
if (f2.exists()) return f2;
// One more level
if (sub.listFiles() != null) {
for (File sub2 : sub.listFiles()) {
if (sub2.isDirectory()) {
File f3 = new File(sub2, name);
if (f3.exists()) return f3;
}
}
}
}
}
}
}
return null;
}
private void mkBat(File dir, String bin) throws Exception {
String skName = bin.replace(".exe","");
try (PrintWriter pw = new PrintWriter(new File(dir,"run.bat"))) {
pw.println("@echo off");
pw.println("cd /d \"%~dp0\"");
pw.println("set PROCESSING_SKETCH_NAME=" + skName);
pw.println("set PROCESSING_SKETCH_PATH=%CD%");
pw.println("start \"\" \"" + bin + "\"");
}
try (PrintWriter pw = new PrintWriter(new File(dir,"launch.vbs"))) {
pw.println("Set sh = CreateObject(\"WScript.Shell\")");
pw.println("sh.CurrentDirectory = CreateObject(\"Scripting.FileSystemObject\").GetParentFolderName(WScript.ScriptFullName)");
pw.println("sh.Run Chr(34) & sh.CurrentDirectory & \"\\\\" + bin + "\" & Chr(34), 0, False");
}
}
private void mkSh(File dir, String bin) throws Exception {
String skName = bin;
File sh = new File(dir,"run.sh");
try (PrintWriter pw = new PrintWriter(sh)) {
pw.println("#!/bin/bash");
pw.println("cd \"$(dirname \"$0\")\"");
pw.println("export PROCESSING_SKETCH_NAME=\"" + skName + "\"");
pw.println("export PROCESSING_SKETCH_PATH=\"$(pwd)\"");
pw.println("./" + bin + " \"$@\"");
}
sh.setExecutable(true);
new File(dir,bin).setExecutable(true);
}
private void mkAppBundle(File dir, String name, File bin, String iconPath) throws Exception {
File app = new File(dir, name + ".app");
File macosDir = new File(app, "Contents/MacOS"); macosDir.mkdirs();
File res = new File(app, "Contents/Resources"); res.mkdirs();
File bundleBin = new File(macosDir, name);
if (bin.exists()) { bin.renameTo(bundleBin); bundleBin.setExecutable(true); }
try (PrintWriter pw = new PrintWriter(new File(app, "Contents/Info.plist"))) {
pw.println("");
pw.println("");
pw.println("");
pw.println(" CFBundleName" + name + "");
pw.println(" CFBundleExecutable" + name + "");
pw.println(" CFBundleIdentifierprocessing.cpp." + name + "");
pw.println(" CFBundleVersion1.0");
pw.println(" CFBundlePackageTypeAPPL");
pw.println(" NSHighResolutionCapable");
if (iconPath != null && !iconPath.isEmpty() && new File(iconPath).exists()) {
pw.println(" CFBundleIconFile" + name + "");
try { copyFile(new File(iconPath), new File(res, name + ".png")); } catch (Exception ignored) {}
}
pw.println("");
}
// run.sh inside bundle
File sh = new File(macosDir, "run.sh");
try (PrintWriter pw = new PrintWriter(sh)) {
pw.println("#!/bin/bash");
pw.println("cd \"$(dirname \"$0\")\"");
pw.println("export PROCESSING_SKETCH_NAME=\"" + name + "\"");
pw.println("./" + name + " \"$@\"");
}
sh.setExecutable(true);
}
private void embedIcon(File bin, ExportTarget t, String iconPng, File outDir) {
try {
if (t.isWindows) embedIconWindows(bin, iconPng, outDir);
else if (t == ExportTarget.LINUX_X64 || t == ExportTarget.LINUX_ARM64
|| t == ExportTarget.LINUX_ARM32) {
// Copy icon.png next to binary for Linux
copyFile(new File(iconPng), new File(outDir, "icon.png"));
// Create .desktop file
File desktop = new File(outDir, bin.getName() + ".desktop");
try (PrintWriter pw = new PrintWriter(desktop)) {
pw.println("[Desktop Entry]");
pw.println("Type=Application");
pw.println("Name=" + bin.getName());
pw.println("Exec=" + bin.getAbsolutePath());
pw.println("Icon=" + new File(outDir, "icon.png").getAbsolutePath());
pw.println("Terminal=false");
}
}
} catch (Exception e) { System.err.println("[icon] " + e.getMessage()); }
}
private void embedIconWindows(File exe, String iconPng, File outDir) throws Exception {
// Convert PNG -> ICO using ImageMagick
File icoFile = new File(outDir, "_icon.ico");
String convert = System.getProperty("os.name","").toLowerCase().contains("win") ? "magick" : "convert";
if (cmdExists(convert)) {
new ProcessBuilder(convert, iconPng,
"-define", "icon:auto-resize=256,128,64,48,32,16",
icoFile.getAbsolutePath())
.redirectErrorStream(true).start().waitFor();
}
if (!icoFile.exists()) {
System.err.println("[icon] ICO conversion failed - copying PNG alongside exe");
copyFile(new File(iconPng), new File(outDir, "icon.png"));
return;
}
// Create .rc and compile with windres
File rcFile = new File(outDir, "_icon.rc");
try (PrintWriter pw = new PrintWriter(rcFile)) {
pw.println("1 ICON \"" + icoFile.getAbsolutePath().replace("\\", "/") + "\"");
}
String windres = cmdExists("x86_64-w64-mingw32-windres")
? "x86_64-w64-mingw32-windres" : "windres";
File resFile = new File(outDir, "_icon.res");
int rc = new ProcessBuilder(windres, rcFile.getAbsolutePath(),
"-O", "coff", "-o", resFile.getAbsolutePath())
.redirectErrorStream(true).start().waitFor();
// Clean up temp files; copy ico alongside exe regardless
copyFile(icoFile, new File(outDir, exe.getName().replace(".exe","") + ".ico"));
icoFile.delete(); rcFile.delete(); resFile.delete();
System.out.println("[icon] Windows icon created alongside exe");
}
private void installExportDeps(java.util.List targets, RunnerListener listener) {
boolean needsWin = targets.stream().anyMatch(t -> t.isWindows);
boolean needsLinux = targets.stream().anyMatch(t ->
t==ExportTarget.LINUX_X64||t==ExportTarget.LINUX_ARM64||t==ExportTarget.LINUX_ARM32);
if (needsWin) {
boolean glfwOk = new File("/usr/x86_64-w64-mingw32/lib/libglfw3.a").exists()
|| new File("/usr/x86_64-w64-mingw32/lib/libglfw3dll.a").exists();
boolean glewOk = new File("/usr/x86_64-w64-mingw32/include/GL/glew.h").exists();
if (!glfwOk || !glewOk) {
listener.statusNotice("Installing Windows cross-compile libs...");
try {
String cmd = cmdExists("yay")
? "yay -S --noconfirm --needed mingw-w64-glfw mingw-w64-glew"
: "pkexec pacman -S --noconfirm --needed mingw-w64-glfw mingw-w64-glew";
new ProcessBuilder("bash","-c",cmd).inheritIO().start().waitFor();
} catch (Exception ignored) {}
}
}
if (needsLinux) {
boolean glfwOk = new File("/usr/lib/libglfw3.a").exists()
|| new File("/usr/lib/libglfw.so").exists();
if (!glfwOk) {
listener.statusNotice("Installing Linux native libs...");
try {
String cmd = cmdExists("pacman")
? "pkexec pacman -S --noconfirm --needed glfw-x11 glew"
: "pkexec apt-get install -y libglfw3-dev libglew-dev";
new ProcessBuilder("bash","-c",cmd).inheritIO().start().waitFor();
} catch (Exception ignored) {}
}
}
}
private void copyFile(File src, File dst) throws Exception {
dst.getParentFile().mkdirs();
Files.copy(src.toPath(), dst.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING);
}
private void copyDir(File src, File dst) throws Exception {
dst.mkdirs();
File[] files = src.listFiles();
if (files == null) return;
for (File f : files) {
if (f.isDirectory()) copyDir(f, new File(dst, f.getName()));
else copyFile(f, new File(dst, f.getName()));
}
}
private boolean mkAppImage(File dir, String binName, String skName,
String iconPath, RunnerListener listener) {
try {
File tools = new File(runtimeDir.getParentFile(), "tools");
File appImageTool = new File(tools, "appimagetool-x86_64.AppImage");
if (!appImageTool.exists()) {
System.err.println("[appimage] appimagetool not found");
return false;
}
// Build AppDir
File appDir = new File(dir, skName + ".AppDir");
File usrBin = new File(appDir, "usr/bin");
usrBin.mkdirs();
// Copy binary
File binary = new File(dir, binName);
File bundledBin = new File(usrBin, skName);
copyFile(binary, bundledBin);
bundledBin.setExecutable(true);
// Copy data/ and font
File dataDir = new File(dir, "data");
if (dataDir.exists()) copyDir(dataDir, new File(usrBin, "data"));
File font = new File(dir, "ProcessingSansPro-Regular.ttf");
if (font.exists()) copyFile(font, new File(usrBin, "ProcessingSansPro-Regular.ttf"));
// Icon - use skName directly, no dashes (Dolphin is picky)
String iconName = skName;
File iconDst = new File(appDir, iconName + ".png");
if (iconPath != null && !iconPath.isEmpty() && new File(iconPath).exists()) {
copyFile(new File(iconPath), iconDst);
} else if (cmdExists("convert")) {
new ProcessBuilder("convert", "-size", "256x256", "xc:#3366cc",
iconDst.getAbsolutePath()).redirectErrorStream(true).start().waitFor();
}
// .DirIcon is required for Dolphin/Nautilus to show the icon
if (iconDst.exists()) copyFile(iconDst, new File(appDir, ".DirIcon"));
// .desktop file
File desktop = new File(appDir, skName + ".desktop");
try (PrintWriter pw = new PrintWriter(desktop)) {
pw.println("[Desktop Entry]");
pw.println("Version=1.0");
pw.println("Type=Application");
pw.println("Name=" + skName);
pw.println("Comment=Made with Processing C++ Mode");
pw.println("Exec=" + skName);
pw.println("Icon=" + iconName);
pw.println("Terminal=false");
pw.println("Categories=Application;");
}
// AppRun - write via Files.write to avoid quote escaping issues
File appRun = new File(appDir, "AppRun");
String appRunContent = "#!/bin/bash\n"
+ "SELF=$(readlink -f \"$0\")\n"
+ "HERE=$(dirname \"$SELF\")\n"
+ "export PROCESSING_SKETCH_NAME=" + skName + "\n"
+ "export PROCESSING_SKETCH_PATH=\"$HERE/usr/bin\"\n"
+ "cd \"$HERE/usr/bin\"\n"
+ "exec \"$HERE/usr/bin/" + skName + "\" \"$@\"\n";
Files.write(appRun.toPath(), appRunContent.getBytes());
appRun.setExecutable(true);
// Run appimagetool
listener.statusNotice("Packaging AppImage...");
File appImageOut = new File(dir, skName + "-x86_64.AppImage");
ProcessBuilder pb = new ProcessBuilder(
appImageTool.getAbsolutePath(), "--no-appstream",
appDir.getAbsolutePath(), appImageOut.getAbsolutePath());
pb.environment().put("ARCH", "x86_64");
pb.redirectErrorStream(true);
StringBuilder log = new StringBuilder();
Process p = pb.start();
try (java.io.BufferedReader br = new java.io.BufferedReader(
new java.io.InputStreamReader(p.getInputStream()))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println("[appimage] " + line);
log.append(line).append('\n');
}
}
int exit = p.waitFor();
if (exit != 0 || !appImageOut.exists()) {
System.err.println("[appimage] failed:\n" + log);
return false;
}
appImageOut.setExecutable(true);
binary.delete();
deleteDir(appDir);
// Create a .desktop wrapper for Dolphin/file managers
// This avoids display rotation issues when launching AppImage from file manager
File desktopLauncher = new File(dir, skName + ".desktop");
String iconPath2 = (iconPath != null && !iconPath.isEmpty() && new File(iconPath).exists())
? iconPath : "";
try (PrintWriter pw = new PrintWriter(desktopLauncher)) {
pw.println("[Desktop Entry]");
pw.println("Version=1.0");
pw.println("Type=Application");
pw.println("Name=" + skName);
pw.println("Comment=Made with Processing C++ Mode");
pw.println("Exec=bash -c \"cd $(dirname %k) && ./" + appImageOut.getName() + "\"");
pw.println("Icon=" + (iconPath2.isEmpty() ? "application-x-executable" : iconPath2));
pw.println("Terminal=false");
pw.println("Categories=Application;");
pw.println("StartupNotify=true");
}
desktopLauncher.setExecutable(true);
System.out.println("[appimage] created: " + appImageOut.getName());
return true;
} catch (Exception e) {
System.err.println("[appimage] " + e.getMessage());
return false;
}
}
private void deleteDir(File d) {
if (!d.exists()) return;
File[] fs = d.listFiles();
if (fs != null) for (File f : fs) { if (f.isDirectory()) deleteDir(f); else f.delete(); }
d.delete();
}
// Compile PNG icon into a Windows .res file for linking
private File compileIconResource(String iconPng, File outDir, ExportTarget t) {
try {
String os = System.getProperty("os.name","").toLowerCase();
boolean onWin = os.contains("win");
// PNG -> ICO using ImageMagick (magick on Windows, convert on Linux)
File icoFile = new File(outDir, "_icon_tmp.ico");
String imageMagick = onWin ? "magick" : "convert";
// On Windows, also try full MSYS2 path
if (onWin && !cmdExists("magick")) {
String[] magickPaths = {
"C:\\msys64\\usr\\bin\\magick.exe",
"C:\\msys64\\mingw64\\bin\\magick.exe",
"C:\\Program Files\\ImageMagick-7.0.0-Q16\\magick.exe"
};
for (String p : magickPaths)
if (new File(p).exists()) { imageMagick = p; break; }
}
if (!cmdExists(imageMagick) && !new File(imageMagick).exists()) {
System.err.println("[icon] ImageMagick not found - skipping icon embed");
System.err.println("[icon] Install ImageMagick: https://imagemagick.org/script/download.php#windows");
return null;
}
int rc = new ProcessBuilder(imageMagick, iconPng,
"-define", "icon:auto-resize=256,128,64,48,32,16",
icoFile.getAbsolutePath())
.redirectErrorStream(true).start().waitFor();
if (rc != 0 || !icoFile.exists()) {
System.err.println("[icon] PNG->ICO conversion failed");
return null;
}
// .rc file - use forward slashes for windres
File rcFile = new File(outDir, "_icon_tmp.rc");
String icoPath = icoFile.getAbsolutePath().replace("\\", "/");
String rcContent = "1 ICON \"" + icoPath + "\"\n";
Files.write(rcFile.toPath(), rcContent.getBytes());
// Find windres - on Windows use MSYS2 windres, on Linux use mingw windres
String windres;
if (onWin) {
windres = "C:\\msys64\\mingw64\\bin\\windres.exe";
if (!new File(windres).exists())
windres = "C:\\msys2\\mingw64\\bin\\windres.exe";
if (!new File(windres).exists())
windres = "windres"; // fallback to PATH
} else {
windres = cmdExists("x86_64-w64-mingw32-windres")
? "x86_64-w64-mingw32-windres" : "windres";
}
File resFile = new File(outDir, "_icon_tmp.res");
StringBuilder windresLog = new StringBuilder();
ProcessBuilder wpb = new ProcessBuilder(windres,
rcFile.getAbsolutePath(), "-O", "coff", "-o", resFile.getAbsolutePath());
wpb.redirectErrorStream(true);
Process wp = wpb.start();
try (java.io.BufferedReader br = new java.io.BufferedReader(
new java.io.InputStreamReader(wp.getInputStream()))) {
String line; while ((line=br.readLine())!=null) windresLog.append(line).append('\n');
}
int rc2 = wp.waitFor();
icoFile.delete(); rcFile.delete();
if (rc2 != 0 || !resFile.exists()) {
System.err.println("[icon] windres failed: " + windresLog);
return null;
}
System.out.println("[icon] Windows icon resource compiled successfully");
return resFile;
} catch (Exception e) {
System.err.println("[icon] compileIconResource: " + e.getMessage());
return null;
}
}
// âÂÂâ Musl cross-compiler toolchains (pre-built, no system install needed) âÂÂâÂÂ
private static final String MUSL_BASE = "https://musl.cc/";
private static final java.util.Map TOOLCHAIN_INFO =
new java.util.LinkedHashMap() {{
put(ExportTarget.LINUX_X64, new String[]{
"x86_64-linux-musl-cross.tgz",
"x86_64-linux-musl-cross/bin/x86_64-linux-musl-g++"});
put(ExportTarget.LINUX_ARM64, new String[]{
"aarch64-linux-musl-cross.tgz",
"aarch64-linux-musl-cross/bin/aarch64-linux-musl-g++"});
put(ExportTarget.LINUX_ARM32, new String[]{
"armv7l-linux-musleabihf-cross.tgz",
"armv7l-linux-musleabihf-cross/bin/armv7l-linux-musleabihf-g++"});
}};
// Get the bundled musl toolchain compiler path for a target
private File getMuslCompiler(ExportTarget t) {
String[] info = TOOLCHAIN_INFO.get(t);
if (info == null) return null;
File toolsDir = new File(runtimeDir.getParentFile(), "tools/cross");
File compiler = new File(toolsDir, info[1]);
return compiler.exists() ? compiler : null;
}
// Check if musl toolchain is installed for target
private boolean muslToolchainInstalled(ExportTarget t) {
return getMuslCompiler(t) != null;
}
// Download and extract musl toolchain with progress dialog
private boolean downloadMuslToolchain(ExportTarget t, RunnerListener listener) {
String[] info = TOOLCHAIN_INFO.get(t);
if (info == null) return false;
String tgzName = info[0];
String url = MUSL_BASE + tgzName;
File toolsDir = new File(runtimeDir.getParentFile(), "tools/cross");
toolsDir.mkdirs();
File tgzFile = new File(toolsDir, tgzName);
// Progress dialog
javax.swing.JDialog prog = new javax.swing.JDialog((java.awt.Frame)null,
"Downloading " + t.label + " toolchain", false);
javax.swing.JProgressBar bar = new javax.swing.JProgressBar(0, 100);
bar.setStringPainted(true);
bar.setString("Connecting...");
javax.swing.JLabel lbl = new javax.swing.JLabel(
" Downloading from musl.cc (~100MB) ");
lbl.setBorder(javax.swing.BorderFactory.createEmptyBorder(8,12,4,12));
prog.setLayout(new java.awt.BorderLayout());
prog.add(lbl, java.awt.BorderLayout.NORTH);
prog.add(bar, java.awt.BorderLayout.CENTER);
prog.pack();
prog.setMinimumSize(new java.awt.Dimension(400, prog.getHeight()));
prog.setLocationRelativeTo(null);
prog.setVisible(true);
try {
// Download with progress
java.net.URL urlObj = new java.net.URL(url);
java.net.HttpURLConnection conn =
(java.net.HttpURLConnection) urlObj.openConnection();
conn.setConnectTimeout(15000);
conn.setReadTimeout(60000);
long total = conn.getContentLengthLong();
listener.statusNotice("Downloading " + tgzName + "...");
try (java.io.InputStream in = conn.getInputStream();
java.io.FileOutputStream out = new java.io.FileOutputStream(tgzFile)) {
byte[] buf = new byte[65536];
long downloaded = 0;
int n;
while ((n = in.read(buf)) > 0) {
out.write(buf, 0, n);
downloaded += n;
if (total > 0) {
final int pct = (int)(downloaded * 100 / total);
final String mb = String.format("%.1f / %.1f MB",
downloaded/1048576.0, total/1048576.0);
javax.swing.SwingUtilities.invokeLater(() -> {
bar.setValue(pct);
bar.setString(mb);
});
}
}
}
conn.disconnect();
// Extract
bar.setString("Extracting...");
bar.setIndeterminate(true);
listener.statusNotice("Extracting " + tgzName + "...");
// Use tar command (available on Linux, Mac, and Windows 10+)
String os = System.getProperty("os.name","").toLowerCase();
ProcessBuilder pb;
if (os.contains("win")) {
pb = new ProcessBuilder("tar", "-xzf",
tgzFile.getAbsolutePath(), "-C", toolsDir.getAbsolutePath());
} else {
pb = new ProcessBuilder("tar", "-xzf",
tgzFile.getAbsolutePath(), "-C", toolsDir.getAbsolutePath());
}
pb.redirectErrorStream(true);
int exitCode = pb.start().waitFor();
tgzFile.delete(); // clean up archive after extraction
prog.dispose();
if (exitCode != 0 || !muslToolchainInstalled(t)) {
listener.statusError("Toolchain extraction failed");
return false;
}
// Make compiler executable (Linux/Mac)
File compiler = getMuslCompiler(t);
if (compiler != null) {
compiler.setExecutable(true);
// Make all binaries in bin/ executable
File binDir = compiler.getParentFile();
if (binDir.listFiles() != null)
for (File f : binDir.listFiles()) f.setExecutable(true);
}
listener.statusNotice(t.label + " toolchain ready");
return true;
} catch (Exception e) {
prog.dispose();
System.err.println("[toolchain] download failed: " + e.getMessage());
javax.swing.SwingUtilities.invokeLater(() ->
javax.swing.JOptionPane.showMessageDialog(null,
"Download failed:
" + e.getMessage() + "
" +
"Check your internet connection and try again.",
"Download Failed", javax.swing.JOptionPane.ERROR_MESSAGE));
if (tgzFile.exists()) tgzFile.delete();
return false;
}
}
}