Igor's Techno Club

Creating cmdhelp tool with GraalVM

I noticed that the most common task I use ChatGPT for is to ask simple questions about command-line:

So, I decided to create a simple ChatGPT wrapper for that task with only one requirement: the installation and usage should be as simple as possible.

While contributing to TruffleRuby, I stumbled upon the GraalVM project, which promised exactly what I needed with the help of native images: creating code in Java and preparing an executable for a particular platform. This would allow running the program without any external dependencies like the JDK. It sounded promising, so I checked how it worked while working on the cmdhelp tool.

Application

In general, the code retrieves the platform OS name and user question, then prepares the ChatGPT prompt, which later sent to the ChatGPT endpoint. Once we have the response, it is printed to stdout.

CommandLineHelper.java

To run the code, you need to build the project and then run the jar file, providing the ChatGPT API keys:

API_KEY="_some_api_key_" java -jar cmdhelp-1.0-SNAPSHOT-jar-with-dependencies.jar "explain drwxr-xr-x"

Native Executables

Ok, we have a jar, but how do we make it run on every environment? For this purpose, I will use the native-image command from the GraalVM project, which does exactly this: it takes the jar, builds it on the targeted environment (e.g., MacOS), and after that, you have a native executable program that doesn't require the JDK anymore.

To run the command, you need to do some preparations or, like in my example, simply run the Docker container where everything is installed and ready to execute the native-image command:

docker run -it --rm --entrypoint /bin/bash ghcr.io/graalvm/native-image-community:22     

For example, you can now upload the jar to that container and run the command to see if it works or not.

At first, the command failed for some obscure reason:

Warning: Reflection method java.lang.Class.getMethods invoked at org.json.JSONObject.populateMap(JSONObject.java:1739)
Warning: Reflection method java.lang.Class.getDeclaredMethods invoked at org.json.JSONObject.populateMap(JSONObject.java:1739)
Warning: Aborting stand-alone image build due to reflection use without configuration.
------------------------------------------------------------------------------------------------------------------------
                        1.0s (9.7% of total time) in 89 GCs | Peak RSS: 1.52GB | CPU load: 10.22
========================================================================================================================
Failed generating 'cmdhelp-1.0-SNAPSHOT-jar-with-dependencies' after 10.1s.
Generating fallback image...
Warning: Image 'cmdhelp-1.0-SNAPSHOT-jar-with-dependencies' is a fallback image that requires a JDK for execution (use --no-fallback to suppress fallback image generation and to print more detailed information on why a fallback image was necessary).

So, I followed the suggestion and reran it with the --no-fallback argument, which helped, allowing me to create a Linux executable.

Automating MacOS Build

So far, I figured out how to build a Linux executable, but my target system was my day-to-day laptop on Apple M1 chip, which means I would need to create a native image for arm64 architecture.

For this task, I used GitHub Actions since it allows executing jobs on any environment, including MacOS.

I set up the job by following the example from the GraalVM action instruction, where to the build matrix, I added only the MacOS system. Once the image is ready, I publish it, and it's ready to be used:

name: GraalVM Native Image builds
on: [push, pull_request]
jobs:
  build:
    name: Build Executables ${{ matrix.os }}
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [macos-latest]
    steps:
      - uses: actions/checkout@v4

      - uses: graalvm/setup-graalvm@v1
        with:
          java-version: '21'
          distribution: 'graalvm'
          github-token: ${{ secrets.GITHUB_TOKEN }}
          native-image-job-reports: 'true'

      - name: Build
        run: |
          mvn install
          cd target
          native-image -jar cmdhelp-1.0-SNAPSHOT-jar-with-dependencies.jar --no-fallback
      
      - name: Upload binary
        uses: actions/upload-artifact@v2
        with:
          name: cmdhelp-${{ matrix.os }}
          path: target/cmdhelp*

Conclusion

I am quite happy with the end result. Without much effort, you can convert a Java application to a native application, which will not require any installation or preparation before running. Just drop it in and start using it. As advertised, the performance of the code should be on par with Java code, but for my use case, it's not that critical.

#ai #cmdhelp #graalvm #java #nativeimage