Running full Node.js from Java/Minecraft, fixing the chain of OSS that's almost there

Posting a little late, but I’ve been working on this long enough and it’s complicated and interesting enough I figure it warrants posting the work I’ve been doing on this for devember.

This started as getting frustrated trying to do web dev stuff for Minecraft mods, and wanting to use my web dev tooling to build real-time data interaction with a Minecraft server.
I found the Grakkit project that provides a JS runtime inside Minecraft, but it uses the GraalVM’s js runtime, which is pretty limited. Someone found what I spent a day googling for and never found, the Javet project that actually embeds Node.JS with modifications and JNI so there’s deep control over the NodeJS runtime, but can still do all my NodeJS stuff.

So here’s where the work I’ve been doing starts. There’s a chain of kind of projects that don’t quite work.
The person who tried combining Javet and Grakkit created Jakkit, but best I can tell mostly relied on intellisense info, since none of the best practices in the Javet docs were implemented, so it sometimes works but leaks memory, and since no one’s using it, there’s no docs or downloads. As well as that, Javet’s CI builds have been broken for a while so they dropped ARM and ARM64 builds, and I have friends who insist on hosting some of my friend groups’ MC servers on RaspberryPi’s.

So to get everything working here are my goals:

Javet:

  • Get Javet’s ARM and ARM64 builds working
  • Get Javet’s github actions working again (some other’s have tried helping with it while I’ve been doing this, but there’s still issues and a lot of optimizations I can contribute)

Jakkit:

  • Write docs for them, can probably borrow a lot from Grakkit’s, but there’s lots of differences with edge cases and stuff
  • Set up github actions to provide downloadable jar's for releases
  • Create ARM and ARM64 builds

Minecraft

  • Get server update events to show in a browser
  • Maybe get browser stuff to mutate the events

I won’t have enough time to write a minecraft mod, but if I do, I’m wanting to build a whole accounting system that conceptually resembles real ERP / main ledger systems, because I’ve been disappointed with the limitations imposed by the Vault APIs every other money mod uses. But doing more complex money stuff really requires tables and stuff, so I really needed web stuff, and the convenient premade UI components I already know how to use.

Where I’m at now:

First, figuring out cross-compilation of NodeJS and v8

So far, I’ve already got arm and arm64 builds working for Javet, which was kind of a nightmare.
Javet compiles both NodeJS and Chromium’s V8 from source with some modifications, so first I tried using the docker build pipeline to emulate native builds for arm, but NodeJS and V8 no longer support native arm builds, so then I had to go the route of cross-compilation.
V8 has tools that mostly handled stuff for me, I just had to translate the naming of the different architecture targets.
For NodeJS I had to search around to find all dependencies I needed, but after enough trial and error I found them all, and got the arm build working, though it required embedding a flag in the environment variable that points to the active cross-compiler. Also I had to figure out how to “install” their custom fork of the RasPi cross-compliers, which was to use a newer version of git than what ships with Ubuntu 20.04, to sparse-checkout the specific directory that contains the cross-compiler I need without downloading many MB of others.
Getting the arm64 build working was less complicated from the point of view of the dependencies since it doesn’t require 34bit stuff, but in trying to do all this I found that the NodeJS build configuration scripts don’t have checks for what flags the targets and cross-compilers support, so I had to write my own python script to edit their build pre-configuration scripts to use or not use some specific flags.
Finally after all that I got both working. (I have up on ia32/i386 since there were a LOT of issues, and I didn’t really need it)

So now I’ve got build scripts running in a docker build pipeline that by changing parameters will build x64, arm, or arm64.
You can see what I’ve got so far here: GitHub - josh-hemphill/Javet at linux-arm64

And now docker

And now I get to address the partial fixes the maintainer of Javet and another contributor added to the github actions and dockerfiles while I was getting the cross-compilations working.
Originally the docker build was defined in two dockerfiles to separate the dev environment from the one that generates artifacts for download.
The main dockerfile that creates the environment and builds NodeJS and V8 is pretty long and complex, so the other contributor decided the correct approach was to split it into several different files and build separate images, one depending on another.
The maintainer expressed they preferred a single file, which I resonated with, and that they wanted to try to keep it to a single command so other can run it without too much trouble.
So on top of adding cross-compilation, I decided to also try to get the more recent feature of Docker/Podman working, multi-stage dockerfiles. (which did ultimately let me set defaults for everything so there’s a short simple command for getting started with it)

So multi-stage dockerfiles are kind of self-explanatory; I had to figure out how to combine layers and stages to limit the sizes, which was a lot of trial and error to make sure all the bash variables and commands were available where they needed to be, but eventually got that figured out.
But then, multi-stage dockerfiles support more complex dependency graphs. To really get down the size you can have stages build stuff, and then only copy specific files or directories into a new stage, resetting everything else to the state of base linux image; and there’s specifiers so I can lock the x64 cross-compile stages to only run on x64 even if it has to emulate it for people running docker on M series Macs, but other stages can be any/all supported architectures and just copy out the files it needs, so that’s what I did.

I ended up with a graph of stages you could simplify to:
javet-docker.drawio

I have a lot more stages than that, but that lets you resume a docker build from more granular points in the build pipeline.

Now I find a bug in Podman/Buildah…

(for those not familiar with Podman, it’s a 1-to-1 replacement for docker, so you can just alias docker to it, though it has some really nice extra features)
So to get this to work, all the stages except the x64-only build stage should run as what ever platforms you’re targeting, so you can run the library in a container on a raspi if you want, without emulation.
But apparently Podman (or it’s builder backend, Buildah) has a bug I discovered, that if you have that earlier common stage, all subsequent directives limiting stages to certain platforms/architectures are ignored… Completely defeating the purpose of all this. If you’re interested, here’s the issue: Multi-stage cross-platform sourcing does not respect `FROM --platform=` when source stage is shared · Issue #4464 · containers/buildah · GitHub
So now I’ve got to decide whether to wait for a fix, write the dockerfiles with a workaround, or stand up another machine or a VM with actual docker and buildkit instead…

Other than that, I think I’ve got the docker bit solved, now I just need to update the github actions to handle things a bit better by using buildkit and caching.

So that’s where I’m at so far … :face_exhaling:

3 Likes

So after all the work on producing containers for each supported platform, I found out Javet’s CMake download doesn’t have a version for armv7, so the final build step for its custom interfaces can’t be done natively and needs to be cross-compiled.

So finding the bug in Buildah was a good learning experience, but now I’m reconfiguring the dockerfile to just run x64 all the way through. Just getting the binaries at the end is good enough.
I’m tempted to just have it build one target archetecture at a time, since the intension is to run on Github Actions, I’ll need to have multiple running separately to avoid reaching the max runtime.
Which comes to the other part, I don’t know that my modifications work yet since a full build of NodeJS and v8 takes a little more than an hour, so I’ve got some waiting to do.

2 Likes

Welp… The last couple days I think have finally convinced me to abandon trying to do armv7 (runnable for RaspberryPi 32bit). I knew people talked about there being issues with building NodeJS for 32bit arm, but the promising articles and forum posts I found made me think I could pull it off. :person_shrugging:
( If anyone’s interested in specifics, I got some errors until I set a bunch of make/gcc environment variables to make sure it was building specifically for the armv7-a instruction set, but then I ran into errors with the included openssl code not supporting that specific instruction set. So perhaps with a lot more hacking of the code and compile scripts it might be possible, but that would be an enormous endeavor by itself.) I think I can get by with just telling my friends that run RaspberryPis that they need to install the arm64 OSs; I think everyone I know is running RaspberryPi 3+ so they should all support 64bit if I remember correctly.

So now that I’m only adding stuff to build for arm64, I just need to strip out all my old extra code from the build steps for 32bit arm and get my docker and github actions optimizations solid.

1 Like

When betting on open-source projects, you need to figure how things are going to progress in the future. If the project dies out, it could be just you trying to push things forward, and the amount of work you have to cover grows quite a lot. This can quickly become impractical.

The exact same logic applies in your paid-work.

Smells to me like betting on bunch of dead-end projects.

Reading your post, I am unsure of your end-goal. You told us your current route, but not a clear destination.

Sounds like you want more(?) insight and more(?) control over the internal state a Mindcraft server. It also sounds like you are trying to add or expand a web-API on the Minecraft server. As a side-goal you are hoping to make this work on a Raspberry Pi, as well as x86 PCs (Linux or Windows?).

Embedding NodeJS into the Mindcraft (Java-based?) server is a clever idea, if it were practical. This does not look practical.

Does the Minecraft server already have a web API? From a quick search, seems that it does. Now I am less clear where you are trying to go.

Are you wanting to use Javascript rather than Java in the Minecraft server? If so, then you could easily embed the Rhino Javascript engine within the Minecraft server. I have made much use of this in past.

Otherwise, what is you destination?

1 Like

On the maintenance of these projects

Usually I would concur, I already have libraries I’ve written that I need to update and maintain better which I’d like to get to;
but in this case, the Javet project is very actively maintained, with corporate usage of fixed versions; the primary maintainer just has no experience with containers and is relying on PRs. He is also limited to pay-per-GB connection and can’t afford to experiment with containers downloading the whole v8 and NodeJS codebases a dozen times. He’s comfortable maintain it, since from here on out it should be mostly just variables he has to update occasionally and there are other community members who’ve tried to help with this, it’s just been a bit janky.

For the Jakkit project, once I can prove the stability and reliability of it, there’s some talk of rolling it into the Grakkit project as a version so you can select your runtime. Jakkit is already maintained by someone else, but I’m not confident they’ll continue so my goal is to get the Grakkit project and community which is quite large to adopt some part of Jakkit so a NodeJS runtime is available.

On the goals

To clear up a little what Javet is. It is not really an embedded runtime like GraalJS or Rhino, as much as it’s linked library bindings to let you run a full NodeJS process as you usually would except that your Java application manages the process and can call into and back out of the NodeJS process. So can use my entire normal NodeJS web dev stack to render web pages, handle web requests, and manage data persistence, without the bottle-neck of sending every possible Java Minecraft event through a network controller to a separately managed NodeJS server/process, making real-time data mutation hopefully much more practical.

Javet itself is used in enterprise environments, so running NodeJS inside a JVM is well supported, just not on arm64 at the moment, and using Javet APIs in place of GraalJS APIs in Grakkit is already mostly done in Jakkit, I will just need to update the instance and memory management to reflect Javet’s extensive documentation on how to do it correctly. So it’s entirely doable, I think it’s actually relatively straight forward from here, it will just take a good bit of work.

On Minecraft’s web APIs: last time I looked, Java Minecraft only exposes basic server metrics via the web; Bedrock Minecraft has some web APIs, but its pretty new and very limited, and it’s also not the variant of Minecraft that everyone I know plays and doesn’t have the rich ecosystem of existing mods.

Small update to what I’ve managed to get done in the last couple days. It seems I’ve managed to go backwards a little; after stripping out all my old scripts for creating arm and x86 builds, my arm64 builds now seem to be failing in some weird ways. It may just be fragments in the docker cache, so now I’ve got to wait an hour or two to find out if that’s the issue or not. :grimacing:

Figured I should post an update. :sweat_smile:
I got stuck for more than a week on issues with CMake detecting the correct C/C++ compilers, and now I’ve got further, now it’s just failing all the java gradle tests at the very end. :grimacing: That’s technically progress, but I’m stuck at another issue that’s left me clueless.

On a more positive note, the Grakkit projects seems to have pulled in the Jakkit codebase and are maintaining their own official Node version of Grakkit, so as soon as I get the arm builds working, there will likely be much less memory management issues to deal with, and more just setting up automated GitHub builds and making sure the correct docs are referenced or available for that version of Grakkit. :face_exhaling: