Sterling has too many projects Blogging about programming, microcontrollers & electronics, 3D printing, and whatever else...

Using Build.pm for complex Perl 6 builds

| 1063 words | 5 minutes | raku
Control panel with wires

I am working on a robotics project with my 3 boys. For this project, I have designed the core components to be a Raspberry Pi, an Adafruit Circuit Playground Express (CPX), and an Adafruit Crickit Hat. The Raspberry Pi will be running firmware written by me to control the Crickit Hat. This firmware will directly drive the sensors, the motors, the servo, and other hardware. The Raspberry Pi also communicates over USB to the CPX to provide high level sensor information and receive commands. My sons will be programming the CPX micro-controllers using Microsoft MakeCode which provides a simple block programming interface for the younger boys and a TypeScript interface to the oldest one.

That’s all fine and relatively straightforward until we try to actually get things talking to each other. The first problem I ran into is that even though every board (as far as I am aware) that MakeCode works with features an on board serial-to-USB chip, MakeCode firmware opts not to use it. And yet, it supports serial communication. Instead of using USB as a serial device (I mean, USB is the Universal Serial Bus after all), they send serial data over a custom HID protocol they call HF2 (the HID Flashing Format, cute abbreviation, eh?). It supports other commands like resetting the firmware and flashing the firmware, but basically the sort of stuff that an Arduino IDE does over USB with the help of the hardware chip. This is silliness in my opinion, but whatever. I will cope.

The coping involves creating a NativeCall wrapper for Perl 6 called Device::HIDAPI, which wraps the hidapi C library. This library can be used to access non-standard HID devices to send and receive HID reports. The same library is portable across Windows, Mac, and Linux. Whee! My main laptop is a Mac and everything is working find when I communicate from my laptop to say XBox controller or the CPX for testing. Cool.

However, when I get ready to test it on Linux, either to run with Travis CI or run on the Pi, I have a new problem. The library on Mac is named libhidapi.dylib which translates to 'hidapi' in the Perl 6 NativeCall interface.

sub hidapi_init(--> int32) is native('hidapi') { * }

However, the library has a different name on Linux. In fact, it can have two possible names. On Linux, the library is named either libhidapi-libusb.so or libhidapi-hidraw.so on Linux because there are two different implementations. This means I need the code above to be effectively:

sub hidapi_init(--> int32) is native('hidapi-libusb') { * }
# OR
sub hidapi_init(--> int32) is native('hidapi-hidraw') { * }

Obviously, I can’t do that. We could start working towards something workable by going to a constant we can easily switch for all methods:

constant HIDAPI = 'hidapi';
sub hidapi_init(--> int32) is native(HIDAPI) { * }

However, I do not want to modify that constant every time I switch between Mac or Linux. That’s a nonstarter. I most certainly don’t want to tell everyone installing it from the ecosystem that they have to fetch it, edit a file, and then build and install it. I’m not a hater.

As an avowed Linux nerd and shell programming geek, the thing I really want to do is something like this:

constant HIDAPI = %*ENV<HIDAPI_LIBARARY>; # WRONG!
sub hidapi_init(--> int32) is native(HIDAPI) { * }

However, while that might look sensible at first blush it will definitely not work the way it looks like it does. What’s the problem? The is native trait will only be set at compile time (i.e., the point when zef install Device::HIDAPI is run). Yet, using an environment variable suggests that this is something that can be set on every run. It won’t be. This is bad news bears. Don’t do it.

It would be possible to use something like no precompilation to force a fresh build every time, but that means Rakudo is going to be generating the stubs and code for NativeCall and compiling the C and the MoarVM byte code every single time the library is used. That’s horrible. I don’t want to do that. I mean, really, setting an environment variable to choose a library name at runtime is a pretty ugly kludge in my opinion anyway. I won’t do it.

My solution is to add a Build.pm to the project. This is a somewhat undocumented feature for building a Perl 6 module (at least, I couldn’t find it on my last search of docs.perl.org), but it is the correct way to introduce any compile-time setup your module requires when distributed through the Perl 6 ecosystem.

Basically, a Build.pm file defines a class named Build that has a method named build which is called at build-time. (Are you seeing pattern here yet?)

use v6;

class Build {
    method build($workdir) {
       # do build-time stuff here
    }
}

The $workdir that is passed is the path to the directory in which the project is being built from. From there, your build method can modify the project in any way necessary to suit the needs of the project.

If you are familiar with autoconf, what comes next will be familiar. To handle the hidapi case, I created 3 nearly identical Perl 6 scripts that each look something like this:

use v6;
use NativeCall;
sub hidapi_init(--> int32) is native('hidapi') { * }
hidapi_init();

I have one for each potential library name. Then I have code that iterates through each name and checks to see if the code runs without error:

    constant LIBS = <hidapi hidapi-hidraw hidapi-libusb>;

    # returns the libraries that run without error
    method try-libraries($workdir --> Seq) {
        gather for LIBS -> $try-lib {
            try {
                EVALFILE "$workdir/test/$try-lib.p6";

                # if we get here, the code didn't blow up
                take $try-lib;
            }
        }
    }

Then, I select the first library found and put it into an auto-generated config package I can reference in my main code:

    method build($workdir) {
        my $lib = first self.try-library($workdir);
        mkdir "$workdir/lib/Device/HIDAPI";
        "$workdir/lib/Device/HIDAPI/Config.pm6".IO.spurt(qq:to/END_OF_CONFIG/);
            # DO NOT EDIT. This file is auto-generated.
            use v6;

            unit package Device::HIDAPI::Config;

            our constant \$HIDAPI = q[$lib];
            END_OF_CONFIG
    }

Now, whenever someone (probably mostly me) installs my module with zef install Device::HIDAPI, this build script will run, test to see which hidapi library is available, and create the configuration file. And with that, I have a pre-compiled and portable builder for my library.

Cheers.

The content of this site is licensed under Attribution 4.0 International (CC BY 4.0).