flypig.co.uk

List items

Items from the current list are shown below.

Newpipe

13 Mar 2025 : Day 4 #
Yesterday we built the NewPipe Extractor for desktop Linux. Our ultimate aim is to get it built for use with Sailfish OS so that the API can be called from C++ or QML code.

But we'll need to spend a bit of time building up to that, because I want to make use of a very specific approach to using Java code on Sailfish OS.

This was something I learnt several years ago from the esteemed Java and Sailfish OS crossover expert Thigg. Back in 2022 I wrote a piece for the Sailfish Community News on a project called Sailing-to-Coffee that Thigg created. Thigg built a pipeline for making use of GraalVM with Sailfish OS.

At the time I thought it was shear brilliance. As time has gone on my opinion hasn't changed and so I'm keen to make use of the technique with NewPipe as well.

I'll let you read the article from the Community News to learn about what's going on. What I wrote there is still valid today. Today, however, I'm going to download the code for Thigg's sailing-the-flood-to-java application, which neatly demonstrates the technique.

If you've been following along closely you'll also note that this approach was also more recently suggested by pherjung, having read about it on the Sailfish OS Forum.

I'm especially gratified that following that, Thigg also got in touch to offer to contribute. We've arranged to have a call to discuss this further and I'm expecting this to add rocket fuel to the project!

Getting the GraalVM pipeline set up is a bit of work, so it'll be helpful to get this successfully running before returning to the NewPipe Extractor code.

To do this I'm going to start by setting up Thigg's sailing-the-flood-to-java application, or at least the Java build part, so that it outputs a native library for use on Sailfish OS built from the Java code in the repository. I've got my own fork of the code and that's where I'm going to start:
git clone git@github.com:llewelld/sailing-the-flood-to-java.git
cd sailing-the-flood-to-java
git submodule update --init --recursive
cd java-part
Unfortunately some things have changed since Thigg set up this repository. In particular it seems Project Lombok now needs to be added as a dependency (maybe it always did, but at least I've found I can't now build it without). This can be added to the plugins section of the pom.xml file. All I've done is add the following inside the build/plugins element:
<plugin>
    <groupId>org.apache.maven.plugins</groupid>
    <artifactId>maven-compiler-plugin</artifactid>
    <version>3.6.1</version>
    <configuration>
        <annotationProcessorPaths>
            <path>
                <groupId>org.projectlombok</groupid>
                <artifactId>lombok</artifactid>
                <version>1.18.30</version>
            </path>
        </annotationprocessorpaths>
    </configuration>
</plugin>
I've also bumped up the Lombok version to 1.18.30:
         <dependency>
             <groupId>org.projectlombok</groupid>
             <artifactId>lombok</artifactid>
-            <version>1.18.24</version>
+            <version>1.18.30</version>
         </dependency>
For context, Lombok doesn't provide any user-facing functionality for the application, but it does make writing Java code easier. For example, rather than having to write getter and setter methods for each field you can just annotate a class with @Data. Similarly you can use @NoArgsConstructor to have Lombok create the constructor for you.

Here's some example code taken from the NewGameParams.java file that illustrates this:
@Data
@NoArgsConstructor
public class NewGameParams {
    public int size;
    public int numColors;
}
This is equivalent to having to write out the following code in full:
@Data
@NoArgsConstructor
public class NewGameParams {
    public int size;
    public int numColors;
    
    public int getSize() { return size; };
    public void setSize(int size) { this.size = size;};
    public int getNumColors() { return numColors; };
    public void setNumColors(int numColors) { this.numColors = numColors; }

    public GameStateT() {
        this.size = 0;
        this.numColors = 0;
    }
}
You can see why people like Lombok. But it does add an extra dependency, so it's inclusion isn't entirely pain free.

Apart from that we also need to update the GraalVM configuration. GraalVM is going to provide us with the real magic, so we need to get this right. In the original version of the pom.xml was the following snippet of XML:
    <dependency>
        <groupId>org.graalvm.sdk</groupid>
        <artifactId>graal-sdk</artifactid>
        <version>22.1.0.1</version>
    </dependency>
This sets up the GraalVM to be a build dependency. But it turns out GraalVM doesn't like this anymore, so I've completely removed it as a dependency. We do still need GraalVM of course, but it's already defined as a plugin for native builds in the same file and it turns out that's as much as we need now.

That sets up the build structure. The next step is to adjust the compile.sh script. This script is unusual for developing code on Sailfish OS because, rather than building things in the Sailfish SDK, it instead copies the code to an actual phone and builds it there.

This may sound a little crazy, but it also makes perfect sense. One of the key motivations for having the Sailfish SDK is that it mimics both the environment and the target architecture (the CPU type: x86, arm, aarch64, etc.) of the phone you want your code to run on.

Why not avoid all that by going directly to the phone and building it there?

Going direct to the phone has a number of benefits. Unless it happens to match the architecture of the system you're working on, the Sailfish SDK has to emulate the target architecture, which can be very slow. The SDK performs a number of clever tricks to try to speed things up, such as switching out emulated tools for native tools where it can, but it still has an impact. Going direct to the phone also relaxes some restrictions that might otherwise apply to the Java compiler, specifically around memory usage.

It's the memory usage restraint that's key for us here. I've tried to set GraalVM up for use within the Sailfish SDK in the past with only limited success. It's possible things have changed in the meantime, so with this project I'm planning to give this another go at some point.

But first things first, let's get a working build.

The next step is to set up GraalVM on a phone. Thankfully all that's needed for this is a directory to unpack the appropriate GraalVM JDK archive into.
$ ssh defaultuser@172.28.172.1
$ mkdir ~/Documents/Development/newpipe/graalvm
$ cd ~/Documents/Development/newpipe/graalvm
$ curl -O https://download.oracle.com/graalvm/23/latest/
    graalvm-jdk-23_linux-aarch64_bin.tar.gz
$ tar -xf graalvm-jdk-23_linux-aarch64_bin.tar.gz
I executed these commands on my phone. Note also that the root of the project folder we're using is /home/defaultuser/Documents/Development/newpipe/. We'll be needing this again in the near future.

The next step is to take a look at and edit the compile.sh shell script. We do this not on the phone but on the device (desktop, laptop,...) we're using to connect to our phone for development. In future I'll refer to this as the orchestrating device, because it's the device that's orchestrating the build.

Here's what I've got in the compile.sh file:
#!/bin/bash
COMPILEHOST=defaultuser@172.28.172.1
COMPILEHOST_WORKSPACE=/home/defaultuser/Documents/Development/newpipe/java-part
COMPILEHOST_GRAAL=/home/defaultuser/Documents/Development/newpipe/graalvm/
    graalvm-jdk-23.0.2+7.1
COMPILEHOST_MAVENLOCAL=/home/defaultuser/Documents/Development/newpipe/graalvm/
    m2
echo &quot;transfering data&quot;
rsync . $COMPILEHOST:$COMPILEHOST_WORKSPACE -r
echo &quot;starting compilation&quot;
ssh $COMPILEHOST &quot;cd $COMPILEHOST_WORKSPACE; 
    GRAALVM_HOME=$COMPILEHOST_GRAAL \
    JAVA_HOME=$COMPILEHOST_GRAAL ./mvnw \
    -Dmaven.repo.local=$COMPILEHOST_MAVENLOCAL clean install -Pnative&quot;
scp $COMPILEHOST:&quot;$COMPILEHOST_WORKSPACE/target/*.h&quot; ../qt-part/lib/
scp $COMPILEHOST:&quot;$COMPILEHOST_WORKSPACE/target/javafloodjava.so&quot; \
    ../qt-part/lib/libjavafloodjava.so
Let's go through this file step-by-step to explain what's going on and so — if you're following along — you can update it with values relevant to your set up.

First we set the COMPILEHOST environment variable. This should contain the endpoint string for logging in to your phone via SSH. Typically this includes a username and an IP address. As you can see, that's what I've got for my setup.

Second we set COMPILEHOST_WORKSPACE to the directory where we want the Java code to be copied over to. Earlier we created a directory /home/defaultuser/Documents/Development/newpipe/ and now we're using a subdirectory of this called java-part, because it's where the JAVA code is going to go.

Next we set the COMPILEHOST_GRAAL environment variable. This should point to the directory where we unpacked the GraalVM to. This contains all of the GraalVM code, as we can see:
$ ls /home/defaultuser/Documents/Development/newpipe/graalvm/
    graalvm-jdk-23.0.2+7.1
GRAALVM-README.md  conf     legal                                man
LICENSE.txt        include  lib                                  release
bin                jmods    license-information-user-manual.zip
The final environment variable we set up is COMPILEHOST_MAVENLOCAL. This is something I added myself because otherwise Maven (the build tool that we're going to use) will store all of the files it downloads to a hidden directory on the phone. I really dislike it when tools do this; it's problematic for many reasons, not least because there's a good chance we'll want to remove all these files later. I always like to know where things are getting stored.

Later on in this script there's a ./mvnw command that gets run on the phone and I've added the COMPILEHOST_MAVENLOCAL reference to this.

There's no need to change the rsync command. This copies the source directory over to the phone ready to be built there.

Although I've edited the proceeding SSH statement, this shouldn't need configuring any further.

At the end there's an scp command which is used to copy the resulting library file that's built off the phone and back on to the orchestrating device.

And that's it. All of the really hard work is going to be done by the mvnw command.

When we execute the script, it will SSH into the phone, build the library and then copy it back, like this:
$ ./compile.sh 
transfering data
starting compilation
[INFO] Scanning for projects...
[INFO] 
[INFO] -----------------------< de.thigg:javafloodjava >-----------------------
[INFO] Building javafloodjava 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[...]

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running GameTest
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.169 sec
Running InterfaceTest
Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.001 sec

Results :

Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
[...]

===============================================================================
GraalVM Native Image: Generating 'javafloodjava' (shared library)...
===============================================================================
For detailed information and explanations on the build output, visit:
https://github.com/oracle/graal/blob/master/docs/reference-manual/native-image/
    BuildOutput.md
-------------------------------------------------------------------------------
[1/8] Initializing...                                          (21.0s @ 0.09GB)
[...]
[2/8] Performing analysis...  [*****]                          (87.7s @ 0.45GB)
    4,354 reachable types   (74.2% of    5,871 total)
    5,622 reachable fields  (48.6% of   11,558 total)
   24,439 reachable methods (50.4% of   48,535 total)
    1,343 types,    14 fields, and   159 methods registered for reflection
       57 types,    57 fields, and    52 methods registered for JNI access
        4 native libraries: dl, pthread, rt, z
[3/8] Building universe...                                     (11.1s @ 0.51GB)
[4/8] Parsing methods...      [*****]                          (26.9s @ 0.58GB)
[5/8] Inlining methods...     [***]                             (9.4s @ 0.66GB)
[6/8] Compiling methods...    [**************]                (220.0s @ 1.09GB)
[7/8] Laying out methods...   [****]                           (13.8s @ 1.25GB)
[8/8] Creating image...       [***]                            (11.4s @ 0.39GB)
[...]
-------------------------------------------------------------------------------
   26.6s (6.4% of total time) in 2369 GCs | Peak RSS: 1.83GB | CPU load: 5.45
-------------------------------------------------------------------------------
Build artifacts:
 newpipe/java-part/target/graal_isolate.h (c_header)
 newpipe/java-part/target/graal_isolate_dynamic.h (c_header)
 newpipe/java-part/target/javafloodjava.h (c_header)
 newpipe/java-part/target/javafloodjava.so (shared_library)
 newpipe/java-part/target/javafloodjava_dynamic.h (c_header)
===============================================================================
Finished generating 'javafloodjava' in 6m 52s.
[...]
graal_isolate.h                                   100% 5426   262.1KB/s   00:00
graal_isolate_dynamic.h                           100% 5538   804.7KB/s   00:00
javafloodjava.h                                   100%  422    74.9KB/s   00:00
javafloodjava_dynamic.h                           100%  478    64.9KB/s   00:00
javafloodjava.so
As you can see, we're left with a juicy-looking javafloodjava.so native library. That's what the C++ code will end up calling.

My plan is to have a similar arrangement to get NewPipe Extractor to build to a native library on my phone. But before doing that we should first also explore how Thigg managed to get the Java methods exposed for use in this library. That's what we'll do tomorrow.

I also want to try once again to get this building on the Sailfish SDK. This would have some benefits, for example it might allow us to build using Chum/OBS. That'll be for a future entry as well.

One final thing I wanted to mention is that yesterday I received a helpful email from Michał Szczepaniak, author of microTube, a YouTube app for Sailfish OS.

Michał suggested that rather than build an entirely new app, an alternative would be to set up NewPipe Extractor as a backend for microTube. He highlighted in particular that he's already had to switch out the backend before, given the projects he's used to access YouTube have had a tendency to come and go. Much like NewPipe, this has left microTube with a good separation between the front-end and backend code.

I personally think this would be a great idea and I'm grateful to Michał for reaching out to me. As this NewPipe project goes on I think it will become clearer whether this is a good fit or not. For example, NewPipe supports multiple services, not just YouTube, so this may or may not fit well with the approach microTube uses. But I'm very much in favour of building up the existing ecosystem rather than fragmenting it, so this is an option I'll be taking a very serious look into.