Skip to content

Conversation

nodtem66
Copy link
Contributor

@nodtem66 nodtem66 commented Jul 31, 2025

Motivation

Currently. Wasp doesn't run natively on Windows OS, which is a pain point for many developers (#2484, #2890). While the program can be used through WSL, the experience can be unpredictable and has known performance issues (#2717). This PR aims to address a specific issue that prevents Wasp from running alias commands on Windows (like npm.ps1, npm.cmd, npx.ps1, npx.cmd), bringing us one step closer to native Windows support.

Description

  • This PR fixed wasp start failing on windows #48 by changing System.Process proc to System.Process shell.
  • This change is a minimal and easy way (compared to Add support for Windows NPM #658) to allow Haskell's System.Process to correctly identify and execute alias commands on Windows, particularly npm and npx which is often a PowerShell script (npm.ps1/npx.ps1) rather than a simple executable.
  • It has been tested on Windows 11. Further testing on both Linux and MacOS is needed to ensure no regressions.

Problem

While running Wasp on Windows, the wasp start command fails because it cannot find npm despite npm and node being present in the user's path.

? --- [Error] Node/NPM requirement not met: --------------------------------------

Running `npm --version` failed.
  `npm --version` command not found!
Make sure you have `npm` installed and in your PATH.

This is because Haskell's System.Process proc struggles to execute PowerShell script (npm.ps1) or Cmd script (npm.cmd), depending on current shell.

Previous PR

The previous PR (#658) solved this problem by separate the logic for executing npm on Windows. It works well, but I have no idea why it has never been merged into the main code.

Solution

The solution is to switch from System.Process proc to System.Process shell ref. This change allows the Haskell process to utilize the system's shell (no matter which shell is), which is better way to handle the alias commands on Windows' PowerShell and Cmd.

Security Considerations

However, the downside of this solution is System.Process shell might be use for executing malicious codes if user-supplied input is passed to the shell. we must ensure that the shell function is only used with a limited and predefined set of commands (like node, npm, npx), and any user provided arguments are properly filtered.

Steps to compile in Windows 11

  • Install ghcup
  • Change directory to waspc
  • Set ghc to version 8.10.7
    ghcup tui
  • Build wasp-cli
    cabal build all

Test suits run test

See commend below.

Unrelated issues on native Windows

Select what type of change this PR introduces:

[x] Bug fix (non-breaking change which fixes an issue).

Update Waspc ChangeLog and version if needed

[x] I updated ChangeLog.md with description of the change this PR introduces.

@nodtem66 nodtem66 changed the title fix #48 Fixes #48: Windows native support Jul 31, 2025
@nodtem66 nodtem66 changed the title Fixes #48: Windows native support Fix #48: Windows native support Jul 31, 2025
@cprecioso
Copy link
Member

Hey @nodtem66, thanks for this contribution!

Since the PR is still a draft, please let us know on the #wasp-dev channel on Discord once it's ready for a full review.

To help us review this effectively, could you please update the PR description to outline the scope of your changes? It's very helpful for us to know:

  • The specific problem this PR intends to solve.
  • Any related issues it intentionally does not cover (i.e., what's out of scope).

After that, please run the test suite with ./run test and share a summary of the results. Knowing which tests are failing (and whether they fall outside the scope of this PR) gives us a perfect overview of its current state and helps us chart a path to getting it merged.
Thanks for the detailed info in advance!

@nodtem66
Copy link
Contributor Author

nodtem66 commented Aug 1, 2025

Hi @cprecioso,

I've added the description that you suggested. The test suit did not pass and produced a long output, so I put it here for reviewing.

test.log

Summary

  • waspc-test: FAIL waspc-test.exe: <stdout>: commitBuffer: invalid argument (invalid character)
  • waspls-test: FAIL (4 out of 11 tests failed (0.01s))
  • cli-test: PASS
  • Test suite e2e-test: SKIP due to using *nix-only commands

I guess it is the problem of Windows EOF, right? Maybe we need to rewrite the test for handling \r\n and \n. What do you think?

Update: I have fixed this issue in #3027

@nodtem66 nodtem66 marked this pull request as ready for review August 1, 2025 09:09
- change `System.Process proc` to `System.Process. shell`
  for running npm.cmd and npm.ps1 commands
- add detail to changeLog.md
@cprecioso
Copy link
Member

Hey @nodtem66, if you don't mind we need to wait this PR as we'd like @Martinsos to review it when he comes back from holidays. So it might be a couple of weeks or so until this gets reviewed.

@nodtem66
Copy link
Contributor Author

nodtem66 commented Aug 5, 2025

This PR is going to patch many POSIX logics that are not compatible to Windows. I would like to PR everything in one go to save time. Currently, I'm working on the prerequisite PR #3027. When it's done, I'll update this PR.

Copy link
Member

@Martinsos Martinsos left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome @nodtem66 , thanks a lot for figuring this out and creating this PR!

So this makes sense to me, and it is great to hear a solution so simple could be the best solution.

As you said, that older PR used a different approach, where it specified process differently when platform was Win. That also works, but this seems simpler + more robust (using shell), as we are letting the system (shell) figure out how to resolve where npm or npx or whatever is, instead of us hardcoding that knowledge in Wasp.

The only thing I am worried about is potential issues that using shell vs proc could cause. As you said, potentially more can be executed than via proc. Wasp is a piece of software that user executes on their own on their own machine and these calls are very controlled, so it is probably fine, but still, I woudl also like to make sure we are not making any kind of regression here. I mostly asked about escaping the arguments, you will see in the comments.

NodeVersion.VersionCheckSuccess -> do
envVars <- getAllEnvVars
let nodeCommandProcess = (P.proc command args) {P.env = Just envVars, P.cwd = Just $ SP.fromAbsDir fromDir}
let nodeCommandProcess = (P.shell fullCommand) {P.env = Just envVars, P.cwd = Just $ SP.fromAbsDir fromDir}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add some comment here, explaining why it is important that we use shell and not proc, so future readers don't wonder why shell was used.

J._jobType = jobType
}
return exitCode
fullCommand = unwords $ command : args
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is one thing I am not sure about: are we missing out on any args escaping here? So before we were giving command and args to P.proc, and I guess it was potentially doing some manipulation with those args. Now it is just one big string (command + args) -> could that be an issue? Should we be wrapping args into ' or something? What could happen if we don't? I am always a bit confused with escaping of args, so we should understand this properly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Martinsos, Thanks for reviewing. I've checked the source code of both P.proc and P.shell. Indeed, P.proc is the modern and safe version of P.shell (see https://github.com/haskell/process/blame/98101f82543b3a28e9f5192d758a508881f8b464/System/Process/Windows.hsc#L423).

I think that we are going to reinvent the wheel by trying to escape args for P.shell. After reviewing these things, I've changed my mind to go for P.proc. (cont. below)

commandResult <-
catchIOError
(Right <$> readProcessWithExitCode commandName commandArgs "")
(Right <$> readCreateProcessWithExitCode (shell fullCommand) "")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm same question here as above, do we need to somehow do something with arguments? fullCommand was used only for printing so that was fine, but now we are passing it to shell.

@Martinsos Martinsos self-assigned this Aug 14, 2025
@Martinsos
Copy link
Member

Martinsos commented Aug 14, 2025

Btw GPT is proposing to use proc "cmd.exe" ["/d","/s","/c", cmdLine] instead of shell, I am not sure if that makes sense or not though. Allegedly should be the same thing, because shell just calls cmd.exe on Winwdows, but we have a bit more control over these flags?

It also advised not to switch to shell on Linux/Mac, only on Windows, since proc should be enough on Linux/Mac, and well it is tighter / more expected.

Yeah this is kind of where I ended up chatting with GPT on this, and it makes sense to me:

  • Linux/macOS → proc "npm" args (no quoting headaches, safe argv splitting, works with normal Node installs).
  • Windows → proc "cmd.exe" ["/d","/s","/c", "npm ...args..."] so that cmd.exe handles .cmd/.ps1 resolution and PATHEXT.

The only “extra” thing on Windows is building that "npm ...args..." string safely — double quotes around anything with spaces, escape double quotes inside, and ^-escape cmd metacharacters outside quotes.

So on Linux/Mac we do it as we were doing so far, with proc, while on Win we also use proc but call "cmd.exe" instead of the actual command and the actual command becomes an argument to it (which we have to properly escape / sanitize).

@nodtem66
Copy link
Contributor Author

nodtem66 commented Aug 15, 2025

@Martinsos
That sounds good to me as well. But, proc "cmd.exe" ["/d","/s","/c", "npm --version"] is a little bit incorrect. I wrote a small program to check this and I would like to propose three solutions using P.proc:

  1. P.proc "cmd" ["/d /s /c npm --version"]
  2. P.proc "cmd" ["/d", "/s", "/c npm --version"]
  3. P.proc "pwsh" ["-CommandWithArgs", "npm --version"]

All arguments in these solution will be escaped automatically by translateInternal

The reason why proc "cmd.exe" ["/d","/s","/c", "npm --version"] doesn't work is related to how cmd.exe handle the string after /c. As you can see below in the test case 4, cmd.exe tried to run "npm --version which is weird.

Test program

import System.IO
import System.Process
import Control.Exception

-- A minimal program to play with System.Process.proc
main :: IO ()
main = do
    putStrLn "--- Test Case 1: Simple command (ls) ---"
    testSimpleCommand
    putStrLn "\n--- Test Case 2: NPM command ---"
    testNPMCommandViaProc
    putStrLn "\n--- Test Case 3: NPM command (shell) ---"
    testNPMCommandViaShell
    putStrLn "\n--- Test Case 4: NPM command (cmd suggested by GPT) ---"
    testNPMCommandViaProcAndCmdSuggestedByGPT
    putStrLn "\n--- Test Case 5: NPM command (cmd with separate argument) ---"
    testNPMCommandViaProcAndCmdWithSeparatedArgument
    putStrLn "\n--- Test Case 6: NPM command (cmd) ---"
    testNPMCommandViaProcAndCmd
    putStrLn "\n--- Test Case 7: NPM command (cmd with escaped argument) ---"
    testNPMCommandViaProcAndCmdWithEscapedArgument
    putStrLn "\n--- Test Case 8: NPM command (PowerShell) ---"
    testNPMCommandViaProcAndPowershell

-- Running a simple command that succeeds.
testSimpleCommand :: IO ()
testSimpleCommand = do
    let p = proc "ls" ["-l"]
    tryCreateProcess p

-- Failing command there is no npm.exe in the path
testNPMCommandViaProc :: IO ()
testNPMCommandViaProc = do
    let p = proc "npm" ["--version"]
    tryCreateProcess p

-- Unsafe way to run a command
testNPMCommandViaShell :: IO ()
testNPMCommandViaShell = do
    let p = shell "npm --version"
    tryCreateProcess p

-- Failed
testNPMCommandViaProcAndCmdSuggestedByGPT :: IO ()
testNPMCommandViaProcAndCmdSuggestedByGPT = do
    -- Run the command using cmd.exe
    -- /c carries out the command specified by string and then terminates
    -- /d disables the execution of AutoRun commands
    -- /s strip the first and last quotes (") around each arguments and leave the rest of the command unchanged
    -- ref: https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/cmd
    let p = proc "cmd" ["/d", "/s", "/c", "npm --version"]
    tryCreateProcess p

-- Failed
testNPMCommandViaProcAndCmdWithSeparatedArgument :: IO ()
testNPMCommandViaProcAndCmdWithSeparatedArgument = do
    let p = proc "cmd" ["/d", "/s", "/c", "npm", "--version"]
    tryCreateProcess p

-- Solution 1: combine everything
testNPMCommandViaProcAndCmd :: IO ()
testNPMCommandViaProcAndCmd = do
    let p = proc "cmd" ["/d /s /c npm --version"]
    tryCreateProcess p

-- Solution 2: only /c command and arguments
testNPMCommandViaProcAndCmdWithEscapedArgument :: IO ()
testNPMCommandViaProcAndCmdWithEscapedArgument = do
    let p = proc "cmd" ["/d", "/s", "/c npm --version"]
    tryCreateProcess p

-- Solution 3: use Powershell
testNPMCommandViaProcAndPowershell :: IO ()
testNPMCommandViaProcAndPowershell = do
    let p = proc "pwsh" ["-CommandWithArgs", "npm --version"]
    tryCreateProcess p

tryCreateProcess :: CreateProcess -> IO ()
tryCreateProcess p = do
    result <- try (createProcess p) :: IO (Either SomeException (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle))
    case result of
        Left err -> putStrLn $ "Error: " ++ show err
        Right (_, _, _, ph) -> do
            exitCode <- waitForProcess ph
            putStrLn $ "Exit Code: " ++ show exitCode

Output

--- Test Case 1: Simple command (ls) ---
total 17
...
...
Exit Code: ExitSuccess

--- Test Case 2: NPM command ---
Error: npm: createProcess: does not exist (No such file or directory)

--- Test Case 3: NPM command (shell) ---
11.4.2
Exit Code: ExitSuccess

--- Test Case 4: NPM command (cmd suggested by GPT) ---
'"npm --version' is not recognized as an internal or external command,
operable program or batch file.
Exit Code: ExitFailure 1

--- Test Case 5: NPM command (cmd with separate argument) ---
node:internal/modules/cjs/loader:1228
  throw err;
  ^

Error: Cannot find module '<path>\node_modules\npm\bin\npm-prefix.js'
    at Function._resolveFilename (node:internal/modules/cjs/loader:1225:15)
    at Function._load (node:internal/modules/cjs/loader:1055:27)
    at TracingChannel.traceSync (node:diagnostics_channel:322:14)
    at wrapModuleLoad (node:internal/modules/cjs/loader:220:24)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:170:5)
    at node:internal/main/run_main_module:36:49 {
  code: 'MODULE_NOT_FOUND',
  requireStack: []
}

Node.js v22.14.0
Exit Code: ExitFailure 1

--- Test Case 6: NPM command (cmd) ---
11.4.2
Exit Code: ExitSuccess

--- Test Case 7: NPM command (cmd with escaped argument) ---
11.4.2
Exit Code: ExitSuccess

--- Test Case 8: NPM command (PowerShell) ---
11.4.2
Exit Code: ExitSuccess

@nodtem66 nodtem66 mentioned this pull request Aug 15, 2025
2 tasks
@Martinsos
Copy link
Member

Some very detailed work @nodtem66 ! Thanks a lot for putting time into this -> I will have to take a bit of time to properly check what you wrote, and the tricky thing is I just started a bit longer (2 weeks) vacation, so forgive me if it will take me a bit to get fully back to you. I will try to get back to you during it, but if not, you can count on me to get on it after the vacation for sure.

@Martinsos
Copy link
Member

@nodtem66 just to let you know I am working on this, I dedicated some time and my understanding is now at a much better level I think, but I still need a bit more time to check all you did here and to make some final recommendations -> coming soon.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

wasp start failing on windows
3 participants