<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Jordie's Blog</title>
        <link>https://bluetainer.nl</link>
        <description>Tech articles for all</description>
        <lastBuildDate>Thu, 13 Mar 2025 18:57:30 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <image>
            <title>Jordie's Blog</title>
            <url>https://bluetainer.nl/favicon.ico</url>
            <link>https://bluetainer.nl</link>
        </image>
        <copyright>CC BY-NC-SA 4.0</copyright>
        <atom:link href="https://bluetainer.nl/rss.xml" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[Observing containerized Java with the OpenTelemetry Java agent and Jib using Gradle]]></title>
            <link>https://bluetainer.nl/blog/opentelemtry-java-agent-with-gradle-and-jib</link>
            <guid>opentelemtry-java-agent-with-gradle-and-jib</guid>
            <pubDate>Thu, 13 Mar 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Since quite some time the OpenTelemetry project has been gaining momentum in cloud-native environments. It standardizes observability across applications and services. The OpenTelemetry team has provided us with amazing zero-code instrumentation for the Java platform, but setting it up can be unintuitive. Today, I will be demonstrating a simple approach using Gradle and Jib.]]></description>
            <content:encoded><![CDATA[<link rel="preload" as="image" href="/public/blog/opentelemtry-java-agent-with-gradle-and-jib/otel-diagram.png"/><h2>What is OpenTelemetry</h2>
<p>The <a href="https://opentelemetry.io/">OpenTelemetry</a> project is a collection of standards, libraries, and applications that
come together to form a vendor-neutral open source observability ecosystem.</p>
<p><img src="/public/blog/opentelemtry-java-agent-with-gradle-and-jib/otel-diagram.png" alt="OpenTelemetry architecture diagram"/></p>
<p>When looking at the overview diagram of the OpenTelemetry ecosystem, we see &quot;OTel Auto. Inst.&quot; in the top left. This is
what we will be focussing on in this article.</p>
<h2>OpenTelemetry Zero-Code Instrumentation</h2>
<p>As part of its suite of libraries, OpenTelemetry provides <a href="https://opentelemetry.io/docs/concepts/instrumentation/zero-code/">zero-code instrumentation</a> for .NET, Go, Java,
JavaScript, PHP, and Python.</p>
<p>The OpenTelemtry documentation the goals of this zero-code instrumentation as succinctly as possible:</p>
<blockquote>
<p>Zero-code instrumentation adds the OpenTelemetry API and SDK capabilities to your application typically as an agent or
agent-like installation. The specific mechanisms involved may differ by language, ranging from bytecode manipulation,
monkey patching, or eBPF to inject calls to the OpenTelemetry API and SDK into your application.</p>
<p>Typically, zero-code instrumentation adds instrumentation for the libraries you’re using. This means that requests and
responses, database calls, message queue calls, and so forth are what are instrumented. Your application’s code,
however, is not typically instrumented. To instrument your code, you’ll need to use code-based instrumentation.</p>
</blockquote>
<p>We will be installing the <a href="https://opentelemetry.io/docs/zero-code/java/agent/">Java agent</a> in a Docker container to monitor our application.</p>
<h2>The Approach</h2>
<p>A long time ago I stumbled upon the following problems:</p>
<ol>
<li>Managing the Dockerfile can be hard.</li>
<li>The syntax for enabling the agent in the Dockerfile is easily broken.</li>
<li>Knowing when a new version of the agent is released can be considered difficult.</li>
<li>The agent is not easily cached across CI builds, this is annoying.</li>
</ol>
<p>I solved all of these problems by using the Gradle <a href="https://docs.gradle.org/current/userguide/distribution_plugin.html">distribution plugin</a> and <a href="https://github.com/GoogleContainerTools/jib">Jib</a>, a
tool built by Google to build optimized Docker and OCI images using a Gradle (or Maven) plugin.</p>
<p>All examples will use the Gradle Kotlin syntax. This has been the <a href="https://blog.gradle.org/kotlin-dsl-is-now-the-default-for-new-gradle-builds">default since 2023</a>.</p>
<p>First, we have to install the <code>distribution</code> and Jib plugins:</p>
<pre filename="build.gradle.kts"><code class="hljs language-kotlin">plugins {
    id(<span class="hljs-string">&quot;java&quot;</span>)
    id(<span class="hljs-string">&quot;distribution&quot;</span>) <span class="hljs-comment">// distribution-base also works &gt;= Gradle 8.13</span>
    id(<span class="hljs-string">&quot;com.google.cloud.tools.jib&quot;</span>) version <span class="hljs-string">&quot;3.4.4&quot;</span>
}
</code></pre>
<p>Before jumping ahead, we will do something unexpected. We will create a <a href="https://docs.gradle.org/current/userguide/declaring_configurations.html#sec:defining-custom-configurations">Gradle configuration</a> to
house our agent:</p>
<pre filename="build.gradle.kts"><code class="hljs language-kotlin"><span class="hljs-keyword">val</span> openTelemetryAgent: Configuration <span class="hljs-keyword">by</span> configurations.creating
</code></pre>
<p>After this, we can use the configuration to define a dependency on the OpenTelemetry Java agent:</p>
<pre filename="build.gradle.kts"><code class="hljs language-kotlin">dependencies {
    <span class="hljs-comment">// ...</span>
    openTelemetryAgent(<span class="hljs-string">&quot;io.opentelemetry.javaagent:opentelemetry-javaagent:2.13.3&quot;</span>)
    <span class="hljs-comment">// ...</span>
}
</code></pre>
<p>Luckily for us, the OpenTelemetry team publishes a Maven artifact for us.</p>
<p>The custom configuration makes sure that we do not include our agent in our final application JAR by accident. It also
makes bots like Dependabot and Renovate aware of the dependency. The dependency is also exposed to
the <a href="https://github.com/ben-manes/gradle-versions-plugin">ben-manes/gradle-versions-plugin</a>, if you use it.</p>
<p>Now, define a distribution that contains our agent. Gradle automatically manages build caching based on the <code>from</code>
method.</p>
<pre filename="build.gradle.kts"><code class="hljs language-kotlin">distributions {
    create(<span class="hljs-string">&quot;openTelemetryAgent&quot;</span>) {
        distributionBaseName = <span class="hljs-string">&quot;otel-agent&quot;</span>
        contents {
            from(openTelemetryAgent)
            rename(<span class="hljs-string">&quot;opentelemetry-javaagent-.*.jar&quot;</span>, <span class="hljs-string">&quot;otel-javaagent.jar&quot;</span>)
        }
    }
}
</code></pre>
<p>Notice that we strip the version number of the Maven artifact. The agent JAR is stored in the
<code>build/install/otel-agent/otel-javaagent.jar</code> directory (when building or calling the distribution installation Gradle
task).</p>
<p>We will now configure Jib (make sure not to forget to import the Jib Gradle plugin):</p>
<pre filename="build.gradle.kts"><code class="hljs language-kotlin">jib {
    container {
        ports = listOf(<span class="hljs-string">&quot;8080/tcp&quot;</span>) <span class="hljs-comment">// Configure the port(s) exposed by your app</span>
        <span class="hljs-comment">// === This is optional-language env vars are usually defined by your base image</span>
        environment = mapOf(
            <span class="hljs-string">&quot;SERVER_PORT&quot;</span> to <span class="hljs-string">&quot;8080&quot;</span>, <span class="hljs-comment">// Works on Spring</span>
            <span class="hljs-string">&quot;LANG&quot;</span> to <span class="hljs-string">&quot;en_US.UTF-8&quot;</span>,
            <span class="hljs-string">&quot;LANGUAGE&quot;</span> to <span class="hljs-string">&quot;en_US:en&quot;</span>,
            <span class="hljs-string">&quot;LC_ALL&quot;</span> to <span class="hljs-string">&quot;en_US.UTF-8&quot;</span>
        )
        <span class="hljs-comment">// === Above here is optional</span>
        jvmFlags = listOf(<span class="hljs-string">&quot;-javaagent:/agent/otel/otel-javaagent.jar&quot;</span>)
    }
    extraDirectories {
        paths {
            path {
                setFrom(layout.buildDirectory.file(<span class="hljs-string">&quot;./install/otel-agent&quot;</span>))
                into = <span class="hljs-string">&quot;/agent/otel&quot;</span>
            }
        }
    }
    from {
        image = <span class="hljs-string">&quot;eclipse-temurin:21&quot;</span>
    }
    to {
        image = <span class="hljs-string">&quot;ghcr.io/my_repository/my_image&quot;</span>
        tags = listOf(<span class="hljs-string">&quot;latest&quot;</span>) <span class="hljs-comment">// Or use an environment variable in your CI</span>
    }
}
</code></pre>
<p>For all options, see the <a href="https://github.com/GoogleContainerTools/jib/tree/master/jib-gradle-plugin">Jib Gradle plugin configuration</a> reference.</p>
<p>As you can see, we copy the <code>otel-javaagent.jar</code> from the build directory into the <code>/agent/otel</code> directory of the image
during built time. We then activate it using <code>-javaagent:/agent/otel/otel-javaagent.jar</code>.</p>
<p>However, we are missing one final step. It is crucial that we let Gradle know that we need the distribution to run our
<code>:jib</code> tasks:</p>
<pre filename="build.gradle.kts"><code class="hljs language-kotlin">tasks {
    <span class="hljs-comment">// ...</span>
    <span class="hljs-keyword">this</span><span class="hljs-symbol">@tasks</span>.jib {
        dependsOn(getByName(<span class="hljs-string">&quot;installOpenTelemetryAgentDist&quot;</span>))
    }
    <span class="hljs-comment">// ...</span>
}
</code></pre>
<p>Don&#x27;t focus too much on the syntax. Without the <code>this@tasks.</code> the wrong thing is referenced.</p>
<p>We are finished! Run the <code>:jib</code> or <code>:jibDockerBuild</code> tasks to create a container image on your local machine and test it
out!</p>
<p>Please refer to the OpenTelemetry documentation to determine what environment variables you can use.</p>
<p>Note that this also works with other Java agents, like the <em>AppDynamics Java Agent</em> or <em>New Relic Java Agent</em>.</p>
<h2>Why not use the Spring Boot Starter?</h2>
<p>The Spring Boot starter can only instrument libraries that Spring manages itself.</p>
<p>For example, when using Log4j2 with Spring, you need to <a href="https://opentelemetry.io/docs/zero-code/java/spring-boot-starter/additional-instrumentations/#log4j2-instrumentation">configure the OpenTelemetry appender yourself</a>.
The agent would pick up its usage automatically.</p>
<p>However, if the agent bytecode scanning and manipulation are taking too long, feel free to drop the agent altogether and
use the manual instrumentation.</p>
<p>It is also worth nothing that Java agents are not supported by GraalVM native images.</p>
<p>Of course, the approach mentioned in this article also works for non-Spring applications.</p>
<h2>Closing Remarks</h2>
<p>Installing the OpenTelemetry Java agent and keeping it up to date can be hard when applying a traditional approach,
mainly because developers are used to editing Dockerfiles by hand</p>
<p>Using a Gradle and Jib makes it easy to integrate the Java agent with your application. It gives you the
added benefit of automatically getting update notifications
when using dependency management software like Dependabot or Renovate.</p>
<p>How have you experienced OpenTelemetry? Let me know!</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Securely transfer files across networks using LocalSend and Tailscale]]></title>
            <link>https://bluetainer.nl/blog/set-up-localsend-with-tailscale</link>
            <guid>set-up-localsend-with-tailscale</guid>
            <pubDate>Sun, 29 Dec 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[Sometimes your devices are connected to different networks, but you need to send a file between them. Using LocalSend and Tailscale, you can quickly and securely transfer your files between these devices.]]></description>
            <content:encoded><![CDATA[<link rel="preload" as="image" href="/public/blog/set-up-localsend-with-tailscale/localsend-on-windows.png"/><link rel="preload" as="image" href="/public/blog/set-up-localsend-with-tailscale/edit-tailscale-ip.png"/><link rel="preload" as="image" href="/public/blog/set-up-localsend-with-tailscale/device-in-localsend.png"/><link rel="preload" as="image" href="/public/blog/set-up-localsend-with-tailscale/sending-over-tailscale.png"/><link rel="preload" as="image" href="/public/blog/set-up-localsend-with-tailscale/edit-acl-tags.png"/><p>Some time ago I discovered the open-source file sharing service <a href="https://localsend.org/">LocalSend</a> whilst browsing the GitHub explore page.</p>
<p>As my devices (phone, laptop, etc.) run on different operating systems, it&#x27;s difficult to share files between them. Some
approaches that I&#x27;ve
used are uploading them to OneDrive, sending myself e-mails, and creating a Bitwarden Send link. This always felt quite
inefficient, and only the last one is truly secure.</p>
<h2>Installing LocalSend</h2>
<p>Installing LocalSend is super easy. On the <a href="https://localsend.org/download">downloads page</a> you can download clients for Windows, macOS, Linux,
Android, and iOS devices. All you need to do now is open the LocalSend application.</p>
<p><img src="/public/blog/set-up-localsend-with-tailscale/localsend-on-windows.png" alt="LocalSend desktop application on Windows"/></p>
<h2>Configuring Tailscale</h2>
<p>Using <a href="https://tailscale.com/">Tailscale</a> is probably the easiest way to connect devices together that I&#x27;ve ever experienced. Because of
it&#x27;s
advanced <a href="https://tailscale.com/blog/how-nat-traversal-works">NAT traversal techniques</a>, it can directly connect your devices together without anyone being able to
perform a MITM attack using the WireGuard protocol. It offers a <a href="https://tailscale.com/blog/free-plan">generous free tier</a> for up to 100 devices. For
the purposes of this article, I will assume that you already have Tailscale installed and connected to all of your
devices.</p>
<blockquote>
<p>Why not use Taildrop?</p>
<p>Recently Tailscale announced their <a href="https://tailscale.com/kb/1106/taildrop">Taildrop</a> file sharing functionality. Whilst this is a cool feature, it
does not support sending files to and from nodes that have ACL tags, a common use case. The user interface of
LocalSend is also better at the moment.</p>
</blockquote>
<p>Unfortunately, LocalSend restricts the multicast mask to <code>/24</code>, this is not enforced by Tailscale by default. Your
device addresses assigned by Tailscale could be <code>100.123.142.91</code> and <code>100.108.230.16</code>, these devices will thus not
be able to find each other.</p>
<p>It&#x27;s possible to create an <a href="https://tailscale.com/kb/1304/ip-pool">IP pool</a> on the access control page that makes sure that future devices added to
your tailnet stay in a certain range:</p>
<blockquote>
<p>Note: This is a beta feature, the syntax might have changed when you are reading this.</p>
</blockquote>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;acls&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">&quot;...&quot;</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;nodeAttrs&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;target&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">&quot;autogroup:member&quot;</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;ipPool&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">&quot;100.x.y.0/24&quot;</span><span class="hljs-punctuation">]</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span>
</code></pre>
<p>You can choose any <code>64 &lt;= x &lt;= 127</code> and any <code>0 &lt;= y &lt;= 255</code> (any address in the range <code>100.64.0.0/10</code>).</p>
<p>If you have already added your devices, you can manually edit the IP of each device that you want LocalSend to
automatically detect:</p>
<p><img src="/public/blog/set-up-localsend-with-tailscale/edit-tailscale-ip.png" alt="Edit an IPV4 address in the Tailscale machine overview"/></p>
<p>After reconnecting Tailscale, your devices will pop up in the LocalSend application:</p>
<p><img src="/public/blog/set-up-localsend-with-tailscale/device-in-localsend.png" alt="Phone visible in the LocalSend Windows application"/></p>
<h2>Transferring Data</h2>
<p>The LocalSend applications are really intuitive. They all follow the same design, as they are made using the
amazing <a href="https://flutter.dev/">Flutter</a> framework.</p>
<p>In the following example I&#x27;ve sent a text message to my phone. My laptop is
connected to Wi-Fi and my phone is connected to 4G. Normally they would not be able to see each other, but because they
are both connected to Tailscale, they can!</p>
<p><img src="/public/blog/set-up-localsend-with-tailscale/sending-over-tailscale.png" alt="Transferring a text message"/></p>
<p><em>Fun fact: I actually sent the screenshot of my phone to my computer using the exact same setup. It only took a couple
of seconds!</em></p>
<h2>Configuring ACL Policies</h2>
<p>Imagine being a household with multiple devices connected to Tailscale.
You might want to limit who can send files to each other using LocalSend. Using access policies you can limit who can
send and receive files:</p>
<pre><code class="hljs language-json"><span class="hljs-punctuation">{</span>
  <span class="hljs-attr">&quot;tagOwners&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
    <span class="hljs-attr">&quot;tag:localsend&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">&quot;autogroup:admin&quot;</span><span class="hljs-punctuation">]</span>
  <span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;acls&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;action&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-string">&quot;accept&quot;</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;src&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">&quot;tag:localsend&quot;</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;dst&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">&quot;tag:localsend:53317&quot;</span><span class="hljs-punctuation">]</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
  <span class="hljs-attr">&quot;nodeAttrs&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span>
    <span class="hljs-punctuation">{</span>
      <span class="hljs-attr">&quot;target&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">&quot;autogroup:member&quot;</span><span class="hljs-punctuation">]</span><span class="hljs-punctuation">,</span>
      <span class="hljs-attr">&quot;ipPool&quot;</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">[</span><span class="hljs-string">&quot;100.91.232.0/24&quot;</span><span class="hljs-punctuation">]</span>
    <span class="hljs-punctuation">}</span>
  <span class="hljs-punctuation">]</span>
<span class="hljs-punctuation">}</span>
</code></pre>
<p><em>Note: I am using IP addresses in the range <code>100.91.232.?/24</code> in my example.</em></p>
<p>Now, configure the ACL tags in the machine overview:</p>
<p><img src="/public/blog/set-up-localsend-with-tailscale/edit-acl-tags.png" alt="Example of edit dropdown in machines tab with &quot;Edit ACL tags&quot; selected"/></p>
<p>Restart Tailscale on all of your devices (and possibly LocalSend), and they will show up!</p>
<h2>Alternatives</h2>
<p>If you are fully bought into the Apple ecosystem, AirDrop is of course an obvious alternative (although
this <a href="https://9to5google.com/2024/12/20/eu-apple-airdrop-airplay-android-more/">might expand to other devices in the near future</a>). The difference with our solution is that we can also
send files to family members or coworkers that might not be in our Bluetooth range.</p>
<p>Another popular open-source alternative is <a href="https://pairdrop.net/">PairDrop</a>. It works a bit differently in the sense that you have
to deploy it to a server. If you ever want to share files with users outside your tailnet, this can be a great hosted
alternative. The benefit of LocalSend is that it&#x27;s a simple desktop application/app that works without needing any
hosting infrastructure or pairing codes.</p>
<p>Let me know if you use any other alternatives 😀.</p>
<h2>Conclusion</h2>
<p>In this article I showed you how to configure Tailscale and LocalSend such that you can securely send files (such as
photos or documents) to devices connected to different networks.</p>
<p>This is great if your phone is having trouble connecting to the Wi-Fi (and you have mobile reception). It is also useful
when you want to quickly send a file to a family members&#x27; laptop down the hallway.
In small business environments you can even set up LocalSend for your distributed team!</p>
<p>I&#x27;ve already used LocalSend a bunch, and it&#x27;s a great product. I&#x27;m excited to hear about your use cases!</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Instantiating a GraalPy (GraalVM) Project with Gradle to Access the Beancount `pip` Package in Java]]></title>
            <link>https://bluetainer.nl/blog/a-graalpy-graalvm-project-with-gradle-beancount-pip-package-java</link>
            <guid>a-graalpy-graalvm-project-with-gradle-beancount-pip-package-java</guid>
            <pubDate>Tue, 12 Dec 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Guide on setting up GraalVM's polyglot interfacing capabilities with Python and `pip` packages.]]></description>
            <content:encoded><![CDATA[<p>Some time ago I stumbled upon a use case that required me to process some data from <a href="https://github.com/beancount/beancount/">Beancount</a> files.
These files follow the <a href="https://plaintextaccounting.org/">Plain Text Accounting</a> philosophy for personal finances.
The Beancount project itself is written in Python, but I wanted to access my data via the Java platform.
There were always ways to do this, but in the past couple of years the concept of <a href="https://en.wikipedia.org/wiki/Polyglot_(computing)">polyglot programming</a> has
gained much traction in the Java community because of the advance of <a href="https://www.graalvm.org/">GraalVM</a>.</p>
<p>This guide will teach you how to set up GraalVM and <a href="https://www.graalvm.org/latest/reference-manual/python/">GraalPy</a> on your Linux or macOS machine
(I am using <a href="https://learn.microsoft.com/en-us/windows/wsl/about">WSL</a>).
After this, we create a Gradle project with the <a href="https://graalvm.github.io/native-build-tools/latest/gradle-plugin.html">Native Build Tools plugin</a>.
When the setup is completed,
we will install the Beancount package using pip and call a (Python) library function from our Java code.</p>
<h2>Installing GraalVM and GraalPy</h2>
<p>There exist multiple ways to install the GraalVM runtime environment.
Because of the otherwise complicated setup process, I have chosen to use <a href="https://sdkman.io/">SDKMAN!</a> and <a href="https://github.com/pyenv/pyenv">pyenv</a>.
Please follow the installation instructions on their respective pages.</p>
<p>First, we want to install GraalVM itself. We can do this using <em>SDKMAN!</em>.
Please ensure that you install the distribution that conforms to <a href="https://www.oracle.com/java/technologies/javase/jdk-faqs.html#GraalVM-licensing">your licence requirements</a>.
I will be referring to the community editions in this article.</p>
<pre><code class="hljs language-shell">sdk install java 21.0.1-graalce
</code></pre>
<p>After this is completed, we can move onto installing GraalPy.
This Python 3.10 compliant runtime makes polyglot programming possible.</p>
<pre><code class="hljs language-shell">pyenv install graalpy-community-23.1.0
</code></pre>
<p>To access the GraalPy environment we can use <code>pyenv shell graalpy-community-23.1.0</code>.</p>
<p>You can verify the installations by running the following commands:</p>
<pre><code class="hljs language-shell"><span class="hljs-meta prompt_">$ </span><span class="bash">java --version</span>
java 21.0.1 2023-10-17
Java(TM) SE Runtime Environment Oracle GraalVM 21.0.1+12.1 (build 21.0.1+12-jvmci-23.1-b19)
Java HotSpot(TM) 64-Bit Server VM Oracle GraalVM 21.0.1+12.1 (build 21.0.1+12-jvmci-23.1-b19, mixed mode, sharing)
<span class="hljs-meta prompt_">$ </span><span class="bash">pyenv shell graalpy-community-23.1.0</span>
<span class="hljs-meta prompt_">$ </span><span class="bash">graalpy --version</span>
GraalPy 3.10.8 (GraalVM CE Native 23.1.0)
</code></pre>
<h2>Creating a Virtual Environment</h2>
<p>We can now create a virtual environment to store our dependencies:</p>
<pre><code class="hljs language-shell">graalpy -m venv venv
</code></pre>
<p>In this example our virtual environment is simply called <code>venv</code>.</p>
<p>To use <code>pip</code> and other commands in this virtual environment, we need to &quot;activate&quot; it.
This can be done by sourcing the correct shell script for your platform, in our case: <code>source ./venv/bin/activate</code>.</p>
<h2>Instantiating Your Gradle Project</h2>
<p>You can instantiate your Gradle anyway you like, or reuse an existing project.</p>
<p>This can be done using <a href="https://docs.gradle.org/current/userguide/part1_gradle_init.html"><code>gradle init</code></a>.</p>
<p>The tutorial will assume a Java <code>application</code> project using the Kotlin DSL.</p>
<p>First of all, we want to make sure we have the <code>application</code> and <em>Native Build Tools</em> plugins installed:</p>
<pre><code class="hljs language-kotlin">plugins {
  application
  id(<span class="hljs-string">&quot;org.graalvm.buildtools.native&quot;</span>) version <span class="hljs-string">&quot;0.9.28&quot;</span>
}
</code></pre>
<p>We also need to define the following dependencies<sup><a href="#user-content-fn-1" id="user-content-fnref-1" data-footnote-ref="true" aria-describedby="footnote-label">1</a></sup>:</p>
<pre><code class="hljs language-kotlin">dependencies {
  implementation(<span class="hljs-string">&quot;org.graalvm.polyglot:polyglot:23.1.0&quot;</span>)
  runtimeOnly(<span class="hljs-string">&quot;org.graalvm.polyglot:python-community:23.1.0&quot;</span>)
  runtimeOnly(<span class="hljs-string">&quot;org.graalvm.polyglot:llvm-community:23.1.0&quot;</span>)
}
</code></pre>
<p>We can define our main class using the <code>application</code> plugin:</p>
<pre><code class="hljs language-kotlin">application {
  mainClass.<span class="hljs-keyword">set</span>(<span class="hljs-string">&quot;org.example.Main&quot;</span>)
}
</code></pre>
<h2>Installing Beancount</h2>
<p>When I first tried to install Beancount using <code>pip install beancount==2.3.6</code> I got the following error:</p>
<pre><code class="hljs language-shell">Compile failed: command &#x27;cc&#x27; failed: No such file or directory
    ...
    creating tmp
    cc -I/usr/include/libxml2 -I/usr/include/libxml2 -c /tmp/xmlXPathInitsdjoqzb0.c -o tmp/xmlXPathInitsdjoqzb0.o
    *********************************************************************************
    Could not find function xmlCheckVersion in library libxml2. Is libxml2 installed?
    *********************************************************************************
    error: command &#x27;cc&#x27; failed: No such file or directory
    [end of output]
  note: This error originates from a subprocess, and is likely not a problem with pip.
  ERROR: Failed building wheel for lxml
Failed to build lxml
ERROR: Could not build wheels for lxml, which is required to install pyproject.toml-based projects
</code></pre>
<p>In this case I was missing <code>cc</code>. On Ubuntu this is included with the <code>build-essential</code> package.
For good measures, I made sure to install all <a href="https://lxml.de/installation.html#requirements">packages that might be necessary to install <code>lxml</code></a>:</p>
<pre><code class="hljs language-shell">sudo apt-get install libxml2-dev libxslt-dev pyhton3-dev build-essential
</code></pre>
<p>Different system dependencies might be necessary,
depending on your operating system and the package you are trying to install.
Always carefully read the error messages produced by <code>pip install</code>,
and check the package documentation to see if you are missing any prerequisites.</p>
<p>It is important to note that installing packages in your virtual environment might take longer than you&#x27;re used to.
This is because the environment also checks if any patches need to be made to the package
or its (sub)dependencies to make it cooperate correctly with GraalPy.</p>
<h2>The Testcase</h2>
<p>To keep it simple, in Beancount, a <em>ledger</em> is the combination of a set of files that contain certain <em>directives</em>.</p>
<p>If this concept is something you want to investigate further,
you might be interested in the <a href="https://beancount.github.io/docs/getting_started_with_beancount.html">Getting Started with Beancount</a> article by the creator of Beancount.
Another good starting point is <a href="https://plaintextaccounting.org/">plaintextaccounting.org</a>.</p>
<p>One of these directives is the <em>commodity directive</em>.
This directive defines a commodity (e.g., currency or stock) that can be referenced in the rest of your ledger.</p>
<p>In our example, we want to read a ledger, extract its directives, and find all commodity symbols (e.g, <code>EUR</code>).</p>
<pre filename="test.beancount"><code class="hljs language-beancount">2023-12-12 commodity EUR
2023-12-12 commodity GBP
2023-12-12 commodity USD
2023-12-12 commodity CHF
</code></pre>
<p>In this example we define the commodities <code>EUR</code>, <code>GBP</code>, <code>USD</code>, and <code>CHF</code>.</p>
<p>Our goal is to create a <code>String[]</code> with these symbols.</p>
<p>In Python these symbols can be found programmatically using the following script:</p>
<pre><code class="hljs language-python"><span class="hljs-keyword">from</span> beancount <span class="hljs-keyword">import</span> loader
<span class="hljs-keyword">from</span> beancount.core.data <span class="hljs-keyword">import</span> Commodity

<span class="hljs-comment"># In this tuple, `entries` is a list of directives</span>
(entries, errors, option_map) = loader.load_file(<span class="hljs-string">&#x27;test.beancount&#x27;</span>)

symbols = [entry.currency <span class="hljs-keyword">for</span> entry <span class="hljs-keyword">in</span> entries <span class="hljs-keyword">if</span> <span class="hljs-built_in">isinstance</span>(entry, Commodity)]

<span class="hljs-built_in">print</span>(symbols) <span class="hljs-comment"># Prints [&#x27;EUR&#x27;, &#x27;GBP&#x27;, &#x27;USD&#x27;, &#x27;CHF&#x27;]</span>
</code></pre>
<h2>Folder Structure for Our Example</h2>
<p>Our project structure should now look somewhat similar to the following:</p>
<pre><code>.
├── build.gradle.kts
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
├── src
│   ├── main
│   │   ├── java
│   │   │   └── org
│   │   │       └── example
│   │   │           └── Main.java
├── test.beancount
└── venv
    ├── bin
    │   ├── activate
    │   ├── bean-check
    │   ├── pip
    │   └── ...
    ├── include
    ├── lib
    │   └── python3.10
    │       └── site-packages
    │           ├── __pycache__
    │           ├── beancount
    │           │   ├── __init__.py
    │           │   └── ...
    │           └── ...
    └── ...
</code></pre>
<p>If this is the case, we can start work on implementing the final solution.</p>
<h2>Implementing Our logic in Java</h2>
<p>When everything is set up correctly, it is trivial to call this exact code in Java.
We are going to use the <a href="https://www.graalvm.org/latest/reference-manual/embed-languages/">GraalVM Polyglot API</a> to run our Python code in the JVM.</p>
<p>We start by creating a <code>Context</code> object,
building these objects can be done by using the <code>Context.Builder</code> builder class:</p>
<pre><code class="hljs language-java"><span class="hljs-keyword">final</span> Context.<span class="hljs-type">Builder</span> <span class="hljs-variable">graal</span> <span class="hljs-operator">=</span> Context.newBuilder(<span class="hljs-string">&quot;python&quot;</span>, <span class="hljs-string">&quot;llvm&quot;</span>)
  .option(<span class="hljs-string">&quot;python.Executable&quot;</span>, <span class="hljs-string">&quot;venv/bin/graalpython&quot;</span>)
  .option(<span class="hljs-string">&quot;python.ForceImportSite&quot;</span>, <span class="hljs-string">&quot;true&quot;</span>)
  .allowIO(IOAccess.ALL)
  .allowNativeAccess(<span class="hljs-literal">true</span>);
<span class="hljs-keyword">try</span> (<span class="hljs-type">Context</span> <span class="hljs-variable">ctx</span> <span class="hljs-operator">=</span> graal.build()) {
  <span class="hljs-comment">// Your code</span>
}
</code></pre>
<p>Within the try-catch block we can use the <code>Context</code> to interact with our guest languages
(in our case these are Python and LLVM).</p>
<p>We need to allow native access because the Beancount package internally uses the <code>struct</code> (and thus <code>pack</code>) builtins.
These builtins are <a href="https://github.com/oracle/graalpython/issues/349#issuecomment-1667349949">implemented as a C module</a>, which requires native access on top of IO access.
Otherwise, we will get the following error: <code>ImportError: cannot import name &#x27;pack&#x27; from &#x27;struct&#x27;</code>.</p>
<p>To make calls to our guest language (Python), we can use the <code>Context#eval(String, String)</code> method:</p>
<pre><code class="hljs language-java"><span class="hljs-comment">// First, import the required packages</span>
ctx.eval(<span class="hljs-string">&quot;python&quot;</span>, <span class="hljs-string">&quot;&quot;&quot;
    from beancount import loader
    from beancount.core.data import Commodity
    &quot;&quot;&quot;</span>);
<span class="hljs-comment">// Then, load the ledger</span>
ctx.eval(<span class="hljs-string">&quot;python&quot;</span>, <span class="hljs-string">&quot;(entries, errors, option_map) = loader.load_file(&#x27;test.beancount&#x27;)&quot;</span>);
<span class="hljs-comment">// Find all symbols</span>
<span class="hljs-keyword">final</span> <span class="hljs-type">Value</span> <span class="hljs-variable">pySymbols</span> <span class="hljs-operator">=</span> ctx.eval(<span class="hljs-string">&quot;python&quot;</span>, <span class="hljs-string">&quot;[entry.currency for entry in entries if isinstance(entry, Commodity)]&quot;</span>);
<span class="hljs-comment">// Convert them to their Java representation</span>
<span class="hljs-keyword">final</span> String[] symbols = pySymbols.as(String[].class);
<span class="hljs-comment">// Finally, print the array (in the Java world)</span>
System.out.println(Arrays.toString(symbols));
</code></pre>
<p>Our full solution now looks like this:</p>
<pre filename="/src/main/java/org/example/Main.java"><code class="hljs language-java"><span class="hljs-keyword">package</span> org.example;

<span class="hljs-keyword">import</span> org.graalvm.polyglot.Context;
<span class="hljs-keyword">import</span> org.graalvm.polyglot.Value;
<span class="hljs-keyword">import</span> org.graalvm.polyglot.io.IOAccess;

<span class="hljs-keyword">import</span> java.nio.file.Path;
<span class="hljs-keyword">import</span> java.nio.file.Paths;
<span class="hljs-keyword">import</span> java.util.Arrays;

<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">Main</span> {
  <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> {
    <span class="hljs-keyword">final</span> Context.<span class="hljs-type">Builder</span> <span class="hljs-variable">graal</span> <span class="hljs-operator">=</span> Context.newBuilder(<span class="hljs-string">&quot;python&quot;</span>, <span class="hljs-string">&quot;llvm&quot;</span>)
          .option(<span class="hljs-string">&quot;python.Executable&quot;</span>, <span class="hljs-string">&quot;venv/bin/graalpython&quot;</span>)
          .option(<span class="hljs-string">&quot;python.ForceImportSite&quot;</span>, <span class="hljs-string">&quot;true&quot;</span>)
          .allowIO(IOAccess.ALL)
          .allowNativeAccess(<span class="hljs-literal">true</span>);
    <span class="hljs-keyword">try</span> (<span class="hljs-type">Context</span> <span class="hljs-variable">ctx</span> <span class="hljs-operator">=</span> graal.build()) {
      ctx.eval(<span class="hljs-string">&quot;python&quot;</span>, <span class="hljs-string">&quot;&quot;&quot;
            from beancount import loader
            from beancount.core.data import Commodity
            &quot;&quot;&quot;</span>);
      ctx.eval(<span class="hljs-string">&quot;python&quot;</span>, <span class="hljs-string">&quot;(entries, errors, option_map) = loader.load_file(&#x27;test.beancount&#x27;)&quot;</span>);

      <span class="hljs-keyword">final</span> <span class="hljs-type">Value</span> <span class="hljs-variable">pySymbols</span> <span class="hljs-operator">=</span> ctx.eval(<span class="hljs-string">&quot;python&quot;</span>, <span class="hljs-string">&quot;[entry.currency for entry in entries if isinstance(entry, Commodity)]&quot;</span>);
      <span class="hljs-keyword">final</span> String[] symbols = pySymbols.as(String[].class);

      System.out.println(Arrays.toString(symbols));
    }
  }
}
</code></pre>
<p>This prints <code>[EUR, GBP, USD, CHF]</code>, exactly what we want.</p>
<p>Verify that you are running the application using the executable built with the <code>nativeBuild</code> task,
or that it is run using the <code>nativeRun</code> task.
The Native Image Tools plugin offers an <a href="https://graalvm.github.io/native-build-tools/latest/gradle-plugin.html#_available_tasks">assortment of tasks</a>.
If you use different tasks (e.g., <code>run</code>),
you will get an error because you are running the application like a &quot;normal&quot; Java application.
This does not offer the polyglot programming capabilities offered by GraalVM.</p>
<h2>Build Time</h2>
<p>On my relatively powerful machine, it takes <a href="https://xkcd.com/303/">almost 8 minutes to compile</a> this example.</p>
<p>When needing to interface with Python fast and securely, there can be advantages to using GraalPy and GraalVM,
but techniques should be applied to make sure development time is not impacted by these long compilation times.</p>
<h2>Conclusion</h2>
<p>In this post, we learned how to set up GraalVM and its Python interface (GraalPy) with the Gradle build tool.</p>
<p>We looked at how to install packages using the <code>pip</code> package manager,
and identified some ways to resolve issues as they occur during package installation in the context of GraalVM.</p>
<p>The polyglot programming capabilities of GraalVM are very powerful when used correctly.
It is, however, sometimes challenging to find the right way to set up projects that make use of certain features.</p>
<p>What can these polyglot capabilities and powerful interfacing techniques bring to your business or project?</p>
<p>The example project is available as <a href="https://github.com/jord1e/graalvm-with-graalpy-and-pip">a repository on GitHub</a>.
The repository also includes a Docker image that shows how we can leverage GraalVM,
GraalPy, and <code>pip</code> in containerized environments.</p>
<section data-footnotes="true" class="footnotes"><h2 class="sr-only" id="footnote-label">Footnotes</h2>
<ol>
<li id="user-content-fn-1">
<p>The LLVM language dependency is necessary for Beancount specifically, your project <em>might</em> not need it. <a href="#user-content-fnref-1" data-footnote-backref="" aria-label="Back to reference 1" class="data-footnote-backref">↩</a></p>
</li>
</ol>
</section>]]></content:encoded>
        </item>
    </channel>
</rss>