The easiest way to prepare a ready-to-use Windows executable on Linux

Hello. The title probably speaks for itself. Is it possible?

Background

I’m developing a pretty simple CLI application translating a binary file to its text description and vice versa. I want it to be portable, so a compiled language is needed (no Perl or Ruby). It will be mostly used by Windows users.

What I have already written, is written in Rust. However, I’m not used to it (I’m not a professional programmer at all), it has its peculiarities and is quite low-level. I want to switch to something at a little higher level. Go doesn’t appeal to me for some reason, but Crystal does.

Creating a ready-to-use Windows executable that just works (under Wine) for a Rust program on Linux has been quite simple. However, I don’t know how to do it for a Crystal “hello world” program without using a Windows instance.

Previous efforts

The resources I found on the Internet pointed out the --cross-compile option, so I tried

crystal build main.cr --cross-compile --target x86_64-pc-win32

but this method doesn’t perform the linking process, it just tells me what command to run on a Window box and I have none (at least not a modern one).

Someone on the Internet mentioned MinGW. I don’t know how to use it, so I just tried

x86_64-w64-mingw32-gcc main.obj -o main.exe

but it produced a lot of undefined reference errors.

At this point I visited the Crystal Discord “server”. Where I explained the issue and asked for help, re:fi.64 tried to help me and after some digging (including downloading the Windows release of Crystal and extracting its libraries) I ended up with

LC_ALL=C x86_64-w64-mingw32-gcc main.obj -o main.exe -L win-lib -lgc -lpcre2-8 -liconv -ldbghelp

but this command didn’t work either, giving over 30 Warning: corrupt .drectve at end of def file (sic) and even more undefined reference.

(Nevertheless, huge thanks to re:fi.64 for trying to help!)

End

Thus, I ask the question in this open forum. Is it possible to easily cross-compile and “cross-link” a Windows executable on Linux? If it is, how to do that?

Kind regards.

1 Like

No. See Build with Mingw-w64 · Issue #6170 · crystal-lang/crystal · GitHub that explains how it could work in theory.

You can cross compile an obj file, then link it in a Windows VM. Maybe you can install MSVC under Wine? I’m not sure how Rust deals with this.

Alternatively, you could build executables in a CI job. It’s generally a very good idea to automate the build process so you don’t have to do it manually.
GitHub Actions for example offers free Windows runners for public projects. We use that to build the Crystal compiler for Windows.

1 Like

Hi Drogoslaw,

As straight-shoota mentioned, I think the easiest way to generate Windows binaries right now is by using GitHub Actions.

Here is a sample GitHub Actions setup from my recent toy project, cowsay.cr. When you tag your Git repository with something like v0.0.2, the compiled binaries will be automatically uploaded to the GitHub Release page.

name: release

on:
  push:
    tags:
      - "v*"

jobs:
  build_release:
    name: Build Release ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu, windows]
    runs-on: ${{ matrix.os }}-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Install Crystal
        uses: crystal-lang/install-crystal@v1
        with:
          crystal: latest

      - name: Run Linux Build
        if: matrix.os == 'ubuntu'
        run: |
          mkdir -p bin
          chmod 755 bin
          docker run -d --name alpine -v $(pwd):/workspace -w /workspace crystallang/crystal:latest-alpine tail -f /dev/null
          docker exec alpine shards install --without-development --release --static
          docker exec alpine shards build --release --static
          docker exec alpine chmod +x bin/cowsay
          docker exec alpine gzip bin/cowsay
          docker exec alpine mv bin/cowsay.gz bin/cowsay-linux.gz

      - name: Run Windows Build x64
        if: matrix.os == 'windows'
        run: |
          shards install --without-development --release
          shards build --release --static
          Set-ExecutionPolicy RemoteSigned -Scope Process
          Compress-Archive -Path bin/cowsay.exe -DestinationPath bin/cowsay-windows.zip

      - name: Upload Release Asset
        uses: softprops/action-gh-release@v2
        with:
          files: |
            bin/cowsay-linux.gz
            bin/cowsay-windows.zip
            LICENSE.txt

This will create statically compiled binaries for both Windows and Linux.
However, I am not an expert, so there might be mistakes in my setup. If you find any errors or have suggestions, please let me know.

  • Permission Settings: You may need to give permissions in the repository settings for GitHub Actions to work correctly.
  • Private Repository Limits: It can be hard to run a lot of GitHub Actions in a private repository.
  • Antivirus Software Issues: Sometimes Windows antivirus software thinks the binaries compiled with GitHub Actions are viruses and may delete them when users download them.

I understand your situation well. I’m not a professional programmer, but I like making command-line tools. I find Crystal easier than Rust, and I don’t like Go very much. It’s nice to use the same tool on both Windows and Linux.

There is also LLVM’s lld-link, but right now you have to manually modify and invoke the cross-link command: Using `lld-link` as a linker · Issue #14332 · crystal-lang/crystal · GitHub

Thank you all for your answers.

The application mentioned in my first message is still in the making, but in the meantime I wrote a small side app and tested GitHub Actions compilation with it.

I managed to set it up and it seems to work! (Note to the other ones looking for help here: it may not be the correct or safe way to do so, it’s my first time with GH Actions.)

Huge thanks especially to @kojix2 on whose GH Actions configuration I based mine.

Could you please help me on how and where to download one of those *.lib files?

lld-link: error: could not open 'pcre2-8.lib': No such file or directory
lld-link: error: could not open 'gc.lib': No such file or directory
lld-link: error: could not open 'libcmt.lib': No such file or directory
lld-link: error: could not open 'iconv.lib': No such file or directory
lld-link: error: could not open 'advapi32.lib': No such file or directory
lld-link: error: could not open 'libvcruntime.lib': No such file or directory
lld-link: error: could not open 'shell32.lib': No such file or directory
lld-link: error: could not open 'ole32.lib': No such file or directory
lld-link: error: could not open 'WS2_32.lib': No such file or directory
lld-link: error: could not open 'kernel32.lib': No such file or directory
lld-link: error: could not open 'Kernel32.lib': No such file or directory
lld-link: error: could not open 'legacy_stdio_definitions.lib': No such file or directory
lld-link: error: could not open 'DbgHelp.lib': No such file or directory
lld-link: error: could not open 'libucrt.lib': No such file or directory

BTW: Github action is too expensive for private project, it would not be an ideal choice for a small private project at all, of course, it may have changed by now.

1 Like

This is why I always create a public repository, even if it is a tiny command-line tool that only I use. However, making the repository public can be difficult for people who are creating web apps.

You can always run the github actions privately using act and if you have a server of any kind you can probably set it up to be triggered via webhooks.

It’s not the same thing, but it can get close.

If you want to go one step further, gitea has an act-based CI thing where you own the runners.

1 Like