Five Considerations When Building Cross-Platform Tools for Windows and macOS

Semgrep has cross-platform native support for Windows, macOS, and Linux.

November 7th, 2025
Share

We thought it would be helpful to share some best practices for dev teams creating command-line security tools with cross-platform requirements. When updating Semgrep Community Edition for first-class support in a Windows environment this year, we discovered compatibility across Linux, macOS, and Windows is more than just file paths. Maintaining a performance and memory profile while taking advantage of low-level operating system behavior is a valuable long-term investment. Semgrep has long had alternatives like Windows Subsystem for Linux (WSL) or a virtual environment / container but native makes the developer experience with installation easier with fewer dependencies. Recent releases reached a milestone where we are confident it is generally available for use.

This post covers some of the subtle pitfalls and lessons learned that development teams should consider that can affect performance, reliability, and even correctness in a rush to support additional environments. Some of the examples are specific to OCaml or Python development projects, but the principles and core concepts apply to many other environments.

1. File and Path Handling: Avoid Regex, Embrace Abstraction

Windows and POSIX (the standards that linux variants and macOS use) fundamentally treat file paths very differently. Any assumptions baked into your code, especially around how paths are represented, can break behavior. The most obvious distinction is that Windows uses backslashes and supports Universal Naming Convention (UNC) paths whereas POSIX uses forward slashes and has a single-rooted file system. 

This difference shows up most with operations like:

  • Comparing paths as strings.

  • Using regular expressions on paths.

  • Writing tests that assert on raw path output.

The fix is simple in principle: treat paths as structured data. In OCaml, that means using Fpath and its equality functions. In Python, we leaned on pathlib.Path. Wherever regexes on paths were unavoidable, we introduced helpers that normalize paths before matching. When testing, we rewrote logic to compare normalized paths or use abstraction layers for file system interactions. This dramatically reduced flakiness in cross-platform test runs.

2. Signals, Subprocesses, and oh fork()

On POSIX systems, it’s common to rely on process forking, running exec() and checking signals like SIGINT. 

This works differently on Windows.

  • Windows doesn’t support fork().

  • The exec*() family of calls does not replace the current process.

  • Signals like SIGINT or SIGALRM are not reliably supported.

Our approach was to refactor process management to use subprocesses across the board. In OCaml, we switched from Unix signal-based timeouts to memory-profiler-based timeouts (via GC.Memprof). In Python, we relied on subprocess instead of any fork/exec logic. We also wrote platform checks to ensure that no POSIX-specific logic runs on Windows.

The takeaway: if you're using signals or fork/exec on POSIX, you'll need Windows-specific alternatives. Write abstractions early, and test both environments.

3. Temporary Files and Deletion Semantics

Temporary files to briefly store data on the filesystem behave differently on Windows. Unlike POSIX, Windows won’t let you delete a file if there are any open handles to it – even in your own process. 

Cases where temporary file differences are commonly encountered:

  • Writing analysis or test results for further processing.

  • Spawning subprocesses that need access to the same data.

  • Cleaning temporary files up to prevent the disk becoming full (Windows doesn’t always do this).

We had to rethink how temporary files are created and used. One strategy was to use Windows-specific flags like OCaml’s O_SHARE_DELETE when opening files. Alternatively ensure that files are explicitly closed before deletion. For shared files, we refactored file creation into higher-level utilities that handle permissions and make cleanup more robust by centralizing the logic.

In some cases, bugs arising from subtle issues like file descriptor duplication behaving differently on Windows. We simplified and flattened file redirection logic to avoid daisy-chaining I/O handles across platforms.

4. Encoding, Pretty Output, and Terminal Behavior

Text encoding is another place where assumptions don’t hold up. On POSIX systems, UTF-8 is generally the default and has been for some time. On Windows however, it’s not the default on older versions of Windows which may still be in use and that can cause a tool to fail or output garbage characters. 

Some of the example uses we observed different behaviors in Python included:

  • Colored CLI output using ANSI escape sequences

  • Reading or writing files with Unicode content

  • Displaying UTF-8 encoded characters in terminal output

We addressed this with a few configuration settings.

Enable the virtual terminal processing build flag (ENABLE_VIRTUAL_TERMINAL_PROCESSING), which allows colored output in Windows terminals to work. Forcing Python to run in UTF-8 mode (PYTHONUTF8=1) can help with encoding issues, when writing to stdout or stderr and can be set explicitly. 

Cross-platform CLI tools should always test output on both platforms, especially if the output includes symbols, colors, or user-supplied text.

5. Build and Distribution: Platform-Specific Python

Building and distributing tools on Windows often require small but critical platform-specific tweaks. For Semgrep, some of the things to watch out for include:

  • Ensuring .exe suffixes were handled in any native build scripts.

  • Replacing unsupported flags like -rpath with Windows alternatives.

  • Accounting for command line length limits.

  • Automatically detecting and bundling DLLs required at runtime.

One step to take would be to audit any DLL dependencies and ensure any required libraries are incorporated into the PyPI wheel. We wrote a script for this and had to patch some build tools that didn’t perform as well on Windows.

CI/CD setup required additional work to enable cross-platform packaging and release, including updates to git configuration and consistent dependency resolution across OSes.

Final Thoughts

Supporting Windows, Linux, and macOS isn’t just about tweaking a few flags. It requires rethinking some of the fundamental assumptions in how your tool interacts with the system. From path handling and signals to encoding and file cleanup, every layer of your tool may be affected. Abstracting operating system interactions early even if only targeting or testing on one platform may save you some headaches later.

Adding native Windows support for Semgrep Community Edition opens up important security tooling to a much broader audience, improved our packaging discipline, and revealed subtle issues that could have affected reliability even on POSIX systems as they evolve later.

If you’re starting down the same path, we hope these lessons help you avoid some of the common traps. And if you’re building a security tool where correctness, clarity, and reliability really matter, cross-platform discipline isn’t just a nice-to-have but a functional requirement.


About

semgrep logo

Semgrep enables teams to use industry-leading AI-assisted static application security testing (SAST), supply chain dependency scanning (SCA), and secrets detection. The Semgrep AppSec Platform is built for teams that struggle with noise by helping development teams apply secure coding practices.