Compiled exe uses Windows GUI subsystem, causing console.log output to be silently discarded
Perry version: 0.5.112
Platform: Windows x64
Linker: MSVC (Visual Studio 2022 Build Tools)
Description
The default project generated by perry init contains console.log() in the entry file:
// Main entry point
function main(): void {
console.log("Hello from Perry!");
}
main();
Compiling and running this produces no output in the terminal:
PS> npx perry compile src/main.ts -o myapp.exe
PS> .\myapp.exe
PS> # <-- no output
perry run also produces no output in the terminal:
PS> npx perry run src/main.ts
Collecting modules...
Found 1 module(s): 1 native, 0 JavaScript
Generating code...
Wrote object file: main_ts.o
Linking (runtime-only)...
Wrote executable: demo
Binary size: 0.5MB
Running demo...
PS> # <-- still no output
Root Cause
The compiled executable has its PE subsystem set to WINDOWS (2) instead of CONSOLE (3):
Subsystem: 2 (3=CONSOLE, 2=WINDOWS)
On Windows, GUI-subsystem processes do not have their stdout/stderr connected to the terminal, so console.log output is silently discarded. This affects both direct execution and perry run, depending on how the terminal captures subprocess output.
The subsystem value can be verified with:
node -e "const fs=require('fs');const buf=fs.readFileSync('myapp.exe');const peOff=buf.readUInt32LE(0x3C);const sub=buf.readUInt16LE(peOff+0x5C);console.log('Subsystem:',sub,'(3=CONSOLE,2=WINDOWS)')"
Verification
Patching the PE subsystem field to CONSOLE (3) fixes the issue:
node -e "const fs=require('fs');const buf=Buffer.from(fs.readFileSync('myapp.exe'));const peOff=buf.readUInt32LE(0x3C);buf.writeUInt16LE(3,peOff+0x5C);fs.writeFileSync('myapp.exe',buf)"
After patching:
PS> .\myapp.exe
Hello from Perry!
Expected Behavior
One of the following would make sense:
- Default to CONSOLE subsystem — Since
perry init generates code using console.log, the compiled exe should produce visible output when run from a terminal.
- Add a
--subsystem compile flag — Allow users to choose between console and windows subsystems (e.g., perry compile src/main.ts -o app.exe --subsystem console).
- Auto-detect subsystem — If the source only uses
perry/system, perry/i18n, etc. (no perry/ui), default to CONSOLE. If it imports perry/ui, use WINDOWS.
- Document the current behavior — At minimum, add a note that
console.log output won't appear in the terminal for the default build, and suggest using perry run during development.
Workaround
Patch the PE subsystem after each compile using the following Node.js snippet:
node -e "const fs=require('fs');const buf=Buffer.from(fs.readFileSync('myapp.exe'));const peOff=buf.readUInt32LE(0x3C);buf.writeUInt16LE(3,peOff+0x5C);fs.writeFileSync('myapp.exe',buf)"
Environment
- Perry: 0.5.112
- OS: Windows 11 x64
- Linker: MSVC link.exe (VS 2022 Build Tools, MSVC 14.44.35207)
perry doctor warning: runtime library: not found (using prebuilt libperry_runtime.a + libperry_stdlib.a)
Compiled exe uses Windows GUI subsystem, causing
console.logoutput to be silently discardedPerry version: 0.5.112
Platform: Windows x64
Linker: MSVC (Visual Studio 2022 Build Tools)
Description
The default project generated by
perry initcontainsconsole.log()in the entry file:Compiling and running this produces no output in the terminal:
perry runalso produces no output in the terminal:Root Cause
The compiled executable has its PE subsystem set to WINDOWS (2) instead of CONSOLE (3):
On Windows, GUI-subsystem processes do not have their stdout/stderr connected to the terminal, so
console.logoutput is silently discarded. This affects both direct execution andperry run, depending on how the terminal captures subprocess output.The subsystem value can be verified with:
node -e "const fs=require('fs');const buf=fs.readFileSync('myapp.exe');const peOff=buf.readUInt32LE(0x3C);const sub=buf.readUInt16LE(peOff+0x5C);console.log('Subsystem:',sub,'(3=CONSOLE,2=WINDOWS)')"Verification
Patching the PE subsystem field to CONSOLE (3) fixes the issue:
node -e "const fs=require('fs');const buf=Buffer.from(fs.readFileSync('myapp.exe'));const peOff=buf.readUInt32LE(0x3C);buf.writeUInt16LE(3,peOff+0x5C);fs.writeFileSync('myapp.exe',buf)"After patching:
Expected Behavior
One of the following would make sense:
perry initgenerates code usingconsole.log, the compiled exe should produce visible output when run from a terminal.--subsystemcompile flag — Allow users to choose betweenconsoleandwindowssubsystems (e.g.,perry compile src/main.ts -o app.exe --subsystem console).perry/system,perry/i18n, etc. (noperry/ui), default to CONSOLE. If it importsperry/ui, use WINDOWS.console.logoutput won't appear in the terminal for the default build, and suggest usingperry runduring development.Workaround
Patch the PE subsystem after each compile using the following Node.js snippet:
node -e "const fs=require('fs');const buf=Buffer.from(fs.readFileSync('myapp.exe'));const peOff=buf.readUInt32LE(0x3C);buf.writeUInt16LE(3,peOff+0x5C);fs.writeFileSync('myapp.exe',buf)"Environment
perry doctorwarning:runtime library: not found(using prebuiltlibperry_runtime.a+libperry_stdlib.a)