Skip to content

Adjust program and args when running executables on Windows#13293

Merged
Alizter merged 1 commit intoocaml:mainfrom
punchagan:win32-proc
Feb 3, 2026
Merged

Adjust program and args when running executables on Windows#13293
Alizter merged 1 commit intoocaml:mainfrom
punchagan:win32-proc

Conversation

@punchagan
Copy link
Copy Markdown
Collaborator

@punchagan punchagan commented Jan 12, 2026

Windows doesn't support shebang lines and build rules generated from opam build instructions could have rules like (build (run <executable-script>)), which would fail to run on Windows. This commit adds support to parse shebang lines of such executable scripts on Windows and run them with an appropriate executable.

Closes #11174

@punchagan punchagan marked this pull request as draft January 12, 2026 10:37
@punchagan punchagan force-pushed the win32-proc branch 2 times, most recently from 8bcdd4b to 5c9bf01 Compare January 12, 2026 13:58
@Alizter
Copy link
Copy Markdown
Collaborator

Alizter commented Jan 12, 2026

Could you add a test in test/blackbox-tests/test-cases/actions/run-shebang.t. You can also mark it with

(cram
 (applies_to run-shebang)
 (alias runtest-windows))

This will run on macOS and Linux by default, and additionally on Windows which is what we want to fix.

@punchagan punchagan force-pushed the win32-proc branch 4 times, most recently from c0baf74 to 5f70449 Compare January 13, 2026 07:46
@punchagan punchagan changed the title WIP: Switch the executable name on win32 Adjust program and args when running executables on Windows Jan 13, 2026
@punchagan punchagan marked this pull request as ready for review January 13, 2026 07:47
@punchagan punchagan force-pushed the win32-proc branch 2 times, most recently from 8a74a42 to 1082424 Compare January 13, 2026 09:05
@punchagan punchagan force-pushed the win32-proc branch 2 times, most recently from 40bc68c to 0179940 Compare January 13, 2026 11:04
Copy link
Copy Markdown
Collaborator

@Leonidas-from-XIV Leonidas-from-XIV left a comment

Choose a reason for hiding this comment

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

Code looks good to me, the implementation makes sense and the tests are fine.

Some minor concerns about wording but otherwise its good to go from my perspective. Thanks for the fix!

Copy link
Copy Markdown
Member

@rgrinberg rgrinberg left a comment

Choose a reason for hiding this comment

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

The most important thing about this change is to make sure that we only do it once per executable.

Also copying @nojb for awareness.

@Alizter Alizter self-requested a review January 13, 2026 22:21
@punchagan punchagan force-pushed the win32-proc branch 2 times, most recently from 38294c9 to 4ad2830 Compare January 14, 2026 06:36
@punchagan
Copy link
Copy Markdown
Collaborator Author

The most important thing about this change is to make sure that we only do it once per executable.

Makes sense. I've now added a Hashtbl to keep track of the executable paths we discovered. Let me know if that's too simplistic and we should do better.

@nojb
Copy link
Copy Markdown
Collaborator

nojb commented Jan 14, 2026

I am not aware of the context, but this seems of limited usefulness to me: is there any reason to expect Windows builds to have executables such as sh, etc available in the environment? Is that the case when using OPAM? Anyway, am not opposed.

@Leonidas-from-XIV
Copy link
Copy Markdown
Collaborator

Can we do the caching per executable with Memo somehow? If we keep track of the input we should be able to memoize the resolved value and also be able to recompute it when the input is updated…

@nojb This is mostly about dune files that use ./foo.sh which breaks even when run in an Unix emulation environment where sh or other commands would be available, simply because Windows does not support shebangs. It copies the same behavior from OPAM.

@nojb
Copy link
Copy Markdown
Collaborator

nojb commented Jan 14, 2026

@nojb This is mostly about dune files that use ./foo.sh which breaks even when run in an Unix emulation environment where sh or other commands would be available, simply because Windows does not support shebangs. It copies the same behavior from OPAM.

Thanks for the pointer. Of course, it would be better for users to call the shell explicitly: (run sh ./foo.sh), but I understand the point of the PR. Thanks!

@Alizter
Copy link
Copy Markdown
Collaborator

Alizter commented Jan 14, 2026

@nojb The decision to change the semantics of run on Windows is something that was discussed previously in:

This PR is intending to resolve the discussion by allowing the same behaviour (of running scripts) across platforms. This is not a simple change and will require careful consideration about whether or not we would like it and if it makes sense for dune as a whole.

Motivation

The motivating use-case for this PR is that when we translate opam build instructions into dune actions and serialise them to a lock file, we cannot know if a given file is an executable or a script without running the action. This means that so far we have only translated such build instructions into (run) actions.

This of course leads into trouble when running such actions on Windows since we can only execute executable files (with extension .exe). There are some edge-cases for handling batch and powershell scripts, but they are not relevant here.

Ultimately, the analogy we draw in Dune between Windows' CreateProcess() and Unix's exec breaks down when it comes to shebangs. Shebangs amongst Unixes are not even standardised. In fact, they are explicitly left undefined in POSIX:

If the first line of a file of shell commands starts with the
characters #!, the results are unspecified.

See here for more details: https://www.in-ulm.de/~mascheck/various/shebang/

Solution

The solution here is (or should be) to understand if a given file is an executable before running it on Windows. If it is, then proceed as usual with CreateProcess() which spawn does for us. However, if it is a script, then we need to inspect the first line, parse it as a shebang, and use whatever interpreter we can understand (if we are aiming for maximum flexibility). I don't know if Windows can detect if a file is/was marked as executable, which might affect things here. There is no "marking as executable" on Windows AFAIK. Git on Windows certainly can do some tracking, but I am not too familiar with the details here.

The main difficulty comes from the flexibility in the definition of a shebang. I am inclined to think that we should be more restrictive with the kinds of interpreters we allow, for example bash and sh in their standard "locations" should be good enough. However there are scripts in the Wild with python3 and even ocaml in the shebang, so we need to make an explicit choice here.

Prior "art"

Other build systems have considered making analogous changes that may be worth reading:

Both cases had a different problem, required the analogous script detection as a possible fix but ultimately ended up with fixing the original problem a different way. That's maybe something we should consider doing here.

Alternatives

For solving the issue with lock directories on Windows, we can consider introducing an alternative run action that is used on all platforms which avoids us from having to make such an invasive change here. The reason this change is invasive here is because its a problem outside of lock directories.

If an author has written:

(rule
 (alias do-thing)
 (deps my-script.sh)
 (action
  (run ./my-script.sh)))

Is there really a good reason to fail this on Windows?

let adjust_prog_and_args ~metadata ~path ?dir prog args =
let { Process.loc; annots; _ } = metadata in
let prog_name = Path.basename prog in
let parse_win32_prog_and_args prog =
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

An alternative:

  let parse_win32_prog_and_args prog =
    match Io.file_line prog 1 with
    | exception _ -> None, []
    | line ->
      match String.drop_prefix line ~prefix:"#!" with
      | None -> None, []
      | Some rest -> parse_shebang_line rest

The string.trim is stripping the trailing \r we get on Windows which is a bit confusing. Also unsure about the binary mode.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Wouldn't Io.file_line prog 1 end up reading a lot of unnecessary data, especially from binary files when a newline character doesn't appear anywhere near the beginning of the file? My implementation tries to read only the first 2 characters before trying to process the first line.

Also, I don't understand what you mean by "stripping the trailing \r" being confusing.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Io.file_line sets binary to false (so Windows line endings turn from \r\n to \n) and skips lines in a loop. It doesn't read the whole file, thus file_line prog 1 is basically just a single input_line on a non-binary file.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I did look at the source of Io.file_line and I understand it that on a non-binary file it does read the first line correctly. But on a binary file, we could end-up reading too many chars, if the newline doesn't appear anywhere the beginning. But, I guess I don't need to worry about it since the DOS Stub ends with a newline, and this code only runs on Windows.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

How does opam do it? Perhaps we should just copy them.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I wrote the initial version after looking at the opam workaround shared by David in #11174 (comment) . I guess, I should've explicitly mentioned that earlier. (I had context from #11174 which others looking at this PR may be missing!)

Copy link
Copy Markdown
Collaborator

@Alizter Alizter left a comment

Choose a reason for hiding this comment

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

I'm happy with the state that this PR is in. If issues arise on Windows with typical build scripts we can of course always revisit this problem.

I have only a few user-facing concerns:

If we see an invalid shebang, what should a user do? We could probably give a little more information in the error message such as the location for the (build) expression and perhaps telling them to report it if they believe its a valid shebang etc.

@Alizter Alizter requested a review from rgrinberg January 21, 2026 12:13
@punchagan punchagan force-pushed the win32-proc branch 5 times, most recently from 80fc7a3 to e22bc8a Compare January 23, 2026 06:47
@punchagan
Copy link
Copy Markdown
Collaborator Author

If we see an invalid shebang, what should a user do? We could probably give a little more information in the error message such as the location for the (build) expression and perhaps telling them to report it if they believe its a valid shebang etc.

I've added a user-facing message to this effect. Let me know if there's anything else I need to fix in this PR, before we are able to merge this.

@Alizter Alizter removed the requires-team-discussion This topic requires a team discussion label Jan 23, 2026
| _ -> prog, args))
in
match parse_win32_prog_and_args prog with
| exception Sys_error _ -> prog, args
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What is throwing exceptions here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Stdune.Io.with_file_in would throw when prog doesn't exist.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ah, I see. Maybe worth defining with_file_in_opt that just wraps Stdune.Io.with_file_in to contain the exception?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I'm happy to do that in a separate PR. I think it's better to keep this one focused on the Windows related change?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This PR introduces the exceptions into this context (thrown by with_file_in but caught around parse_win32_prog_and_args), so it would make sense to keep the code exception-free from the start instead of merging it and then removing it later.

@Alizter
Copy link
Copy Markdown
Collaborator

Alizter commented Feb 2, 2026

As we've discussed before. There were a few more changes I would like:

  • #! on its own line gives End_of_file from input_line
  • having a space after a shebang and before \r\n causes some issues with the parsing
  • removing the hash table (I think this was done?)

@punchagan punchagan force-pushed the win32-proc branch 2 times, most recently from 5bfb134 to eb8fc1c Compare February 2, 2026 10:40
@punchagan
Copy link
Copy Markdown
Collaborator Author

Thanks for listing out the changes!

  • #! on its own line gives End_of_file from input_line

I've added a test case for this and tweaked the code to handle this correctly.

* having a space after a shebang and before `\r\n` causes some issues with the parsing

I've added a test case for this, but couldn't find any issues. If you have an example handy, that would be helpful.

* removing the hash table (I think this was done?)

Yes, I've removed this.

@Alizter Alizter requested review from rgrinberg and removed request for rgrinberg February 2, 2026 11:15
Windows doesn't support shebang lines and build rules generated from
opam build instructions could have rules like `(build (run
<executable-script>))`, which would fail to run on Windows. This commit
adds support to parse shebang lines of such executable scripts on
Windows and run them with an appropriate executable.

Closes ocaml#11174

Signed-off-by: Puneeth Chaganti <punchagan@muse-amuse.in>
Copy link
Copy Markdown
Member

@rgrinberg rgrinberg left a comment

Choose a reason for hiding this comment

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

LGTM.

I think it would be better to test this behavior on every OS by making it possible to enable it in a config. Few of us have access to Windows otherwise.

@Alizter Alizter merged commit 2d96f06 into ocaml:main Feb 3, 2026
28 checks passed
@punchagan punchagan deleted the win32-proc branch February 3, 2026 04:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

windows Issues that relate to Dune on Microsoft Windows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

On windows, (run ./configure) results in CreateProcess(): Exec format error

6 participants