Building Windows executables with GraalVM on Travis CI
I am not very well acquainted with Windows and I spent an entire day figuring out how to use GraalVM Native Image on Windows and I know how frustrating that process can be. Given how uncomplete the documentation for Windows is and how many people are confused about it on forums I think writing about it may save unnecessary effort and frustration.
I will present how to do it both on Windows desktop and programmatically on a remote Windows environment, which matters for CI. Since Native Image does not support cross-compilation you need to compile your application on Windows machine if you want to distribute Windows binaries. I used Travis CI Windows environment for that purpose.
When using Native Image you should be aware of its limitations in regards to the use of reflection or static initialization. In this article I assume you made Native Image work for your input JAR on Linux or Mac and your problem is specifically with Windows.
I will use GraalVM 20.0.0. Starting from 20.0.0:
Windows is no longer an experimental platform in the GraalVM ecosystem. Windows builds now contain the functional gu utility to install the components. GraalVM Native Image component needs to be installed with gu as on other platforms.
Additionally, starting from 20.0.0, you can find GraalVM artifacts for Windows at Github releases, as well as for other platforms. In this guide I will install GraalVM JDK with SKDMAN but it's good to be aware of the alternative.
Prerequisites
- chocolatey - package manager for Windows
- Git Bash - we're not even interested that much in git as we are in bash
Note for Travis CI: all prerequisites are already installed on Travis CI's Windows environment and Git Bash is the shell that’s used to run your build. So you don't need to do anything here.
Install Java 11 based GraalVM
All bash snippets are supposed to be run from Git Bash unless I specifically note any other one:
choco install zip unzip
choco install visualstudio2017-workload-vctools
curl -sL https://get.sdkman.io | bash
mkdir -p "$HOME/.sdkman/etc/"
echo sdkman_auto_answer=true > "$HOME/.sdkman/etc/config"
echo sdkman_auto_selfupdate=true >> "$HOME/.sdkman/etc/config"
"source $HOME/.sdkman/bin/sdkman-init.sh"
sdk install java 20.0.0.r11-grl
zip
and unzip
are needed by SDKMAN. visualstudio2017-workload-vctools
to get Windows compiler toolchain which will be used by native-image
. Then we installed SDKMAN to use it for installing 20.0.0.r11-grl
. r11
stands for Java 11 JDK and grl
stands for GraalVM
.
Install native-image
gu.cmd install native-image
Build binary using native-image
Let's try to build hello-world program written in Java. I prepared sbt project with hello world application and built jar with sbt assembly
. It does not really matter for the essence of the article how you built your JAR.
Now it's time to finally run native-image
:
native-image.cmd --verbose --static --no-fallback -H:+ReportExceptionStackTraces -jar Main.jar main
This is roughly an output you will receive:
...
[main:12556] classlist: 1,145.18 ms, 1.00 GB
[main:12556] (cap): 113.18 ms, 1.00 GB
[main:12556] setup: 581.30 ms, 1.00 GB
Error: Unable to compile C-ABI query code. Make sure native software development toolchain is installed on your system.
If you inspect stack traces close you will also find:
Caused by: java.io.IOException: Cannot run program "CL" (in directory "C:\Users\user\AppData\Local\Temp\SVM-697645000753775759"): CreateProcess error=2, The system cannot find the file specified
Seems like native-image misses CL
command and if we try to run 'CL' in Git Bash:
$ CL
bash: CL: command not found
So our task now is to bring CL
into scope.
We installed visualstudio2017-workload-vctools
at the beginning so the toolchain is installed but it's neither available from Git Bash nor from standard cmd
. It took some time and desperation to eventually find the answer at Github that you need to run x64 Native Tools Command Prompt for VS 2017
. That means that on Windows desktop you need to press Windows key and type in x64 Native Tools Command Prompt for VS 2017
. On Travis CI you need to execute C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\VC\Auxiliary\Build\vcvars64.bat
.
Let's try to do that. Since Git bash replaces Windows back slashes into Linux slashes we can do the following:
$ /c/Program\ Files\ \(x86\)/Microsoft\ Visual\ Studio/2017/BuildTools/VC/Auxiliary/Build/vcvars64.bat
**********************************************************************
** Visual Studio 2017 Developer Command Prompt v15.0
** Copyright (c) 2017 Microsoft Corporation
**********************************************************************
[vcvarsall.bat] Environment initialized for: 'x64'
Looks promising but we quickly realize that CL
is still not available:
$ CL
bash: CL: command not found
If we try to do the same in cmd
(i.e. press Windows key and type in cmd
) then:
>call "C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\VC\Auxiliary\Build\vcvars64.bat"
**********************************************************************
** Visual Studio 2017 Developer Command Prompt v15.0
** Copyright (c) 2017 Microsoft Corporation
**********************************************************************
[vcvarsall.bat] Environment initialized for: 'x64'
>CL
Microsoft (R) C/C++ Optimizing Compiler Version 19.16.27035 for x64
Copyright (C) Microsoft Corporation. All rights reserved.
usage: cl [ option... ] filename... [ /link linkoption... ]
That looks really good! How to achieve that on Travis CI environment? We can create build.bat
in which we can write in Windows Batch. Create file build.bat
:
call "C:\Program Files (x86)\Microsoft Visual Studio\2017\BuildTools\VC\Auxiliary\Build\vcvars64.bat"
%HOME%/.sdkman/candidates/java/current/bin/native-image.cmd --verbose --static --no-fallback -H:+ReportExceptionStackTraces -jar Main.jar main
There was one more problem to solve on the way - how to invoke native-image.cmd
from standard, non Git Bash, console. I utilized the sdkman directory structure and used %HOME%/.sdkman/candidates/java/current/bin/native-image.cmd
.
Now we can call ./build.bat
from Git Bash. If we do so and if everything went well the last lines of output are expected to be:
...
[main:15708] image: 711.60 ms, 1.92 GB
[main:15708] write: 379.96 ms, 1.92 GB
[main:15708] [total]: 20,697.84 ms, 1.92 GB
Now you should be able to run the result:
$ ./main.exe
Hello world!
Making it work on fresh Windows system
It may look like everything worked but as noted here if you run this executable on systems without Visual Studio toolchain it will fail with:
The code execution cannot proceed because VCRUNTIME140.dll was not found.
You can fix it by installing Microsoft Visual C++ 2017 Redistributable
or by redistributing VCRUNTIME140.dll
next to your executable. As long both an executable and a dll file are in the same folder it should work. According to Microsoft docs it's fine from license point of view (as VCRUNTIME140.dll
is distributed in FIXME
To be sure you executable works without any dependencies I suggest running it on fresh Windows installation using a virtual machine.
Distributing binary using Github releases page
Since a single file is not enough we will create a zip containing VCRUNTIME140.dll
side by side to exe
:
cp /c/Windows/System32/VCRUNTIME140.dll .
zip main.zip main.exe VCRUNTIME140.dll
Deploy part of .travis.yml
:
deploy:
provider: releases
api_key: $GITHUB_TOKEN
file: main.zip
skip_cleanup: true
on:
tags: true
To make it work you need to:
* generate Personal access token
at https://github.com/settings/tokens
* add environment variable GITHUB_TOKEN
at Travis CI project settings and set it to token value from the previous point
Repository with a reproducer
As a working code is worth a thousand words I prepared a repository containing all you need to reproduce the solution.
References
I found those two articles especially useful: Build great native CLI apps in Java with GraalVM and Picocli and GraalVM native image on Windows.
You can track the discussion on cross compilation support in Native Image here.