00:00:14.760
Hello and welcome to the talk on building maintainable command-line tools with MRuby. I am Eric Hodel, and this presentation will focus on working with MRuby and mruby-cli.
00:00:22.540
A bit about me: I am a Ruby committer, a rubygems committer, and the author of many gems. Currently, I’m on vacation from most of my open-source work. Instead of working on Ruby in my spare time, I have been spending a lot of time making shelves, like the one shown on the screen.
00:00:35.020
The far shelf is still a work in progress; I’m trying to smooth out some gaps in it. I work for Fastly, a real-time content delivery network, and this is our amazing website, fastly.com, featuring Gordo, one of the dogs at Fastly.
00:00:46.180
Fastly supports many open-source projects, including hosting the Ruby language and providing downloads for RubyGems. Last month, Fastly served 425 terabytes of data and handled eight billion requests, supporting RubyGems for free. I’d also like to highlight the work of David Radcliffe and the rest of the rubygems.org team for speeding up gem downloads, especially for users outside the United States. They will be making a blog post about that work soon.
00:01:11.170
You can follow Fastly on Twitter. Now, moving on to part one: my exploration of MRuby and mruby-cli. We’re building a new product at Fastly with new APIs, and I wanted an easy way to interact with those APIs to demonstrate their usability.
00:01:30.130
Why not choose Ruby to write the wrapper? When using CRuby, developers must install the correct version depending on which Ruby features may be needed. They also have to install the gems they depend on and keep both the Ruby version and the gems up to date for the duration of tool support.
00:01:44.590
MRuby is very similar to CRuby, so we can start with a prototype in CRuby, port it to MRuby, and then handle maintenance in MRuby. But what makes MRuby more suitable than CRuby? MRuby is a lightweight Ruby implementation; you only use the features of Ruby that you need, which keeps the language size smaller.
00:02:06.250
It is designed for embedding; you can run Ruby on low-resource hardware or embed it into other software without the hassles of embedding CRuby. MRuby has libraries, and it calls them gems. It ships with a limited standard library, so you can use a gem for an array or a gem to access the full features of CRuby.
00:02:30.700
There are also standalone MRuby C libraries, some of which are C extensions that wrap libraries, like the MRuby curl gem. You can find many talks on MRuby, including one titled 'Making High Tech Seat in MRuby,' which is about building a robot with Lego Mindstorms.
00:02:43.310
You can access recordings of these talks on the RubyKagi website. This year’s RubyConf features two talks on MRuby; one is in Ballroom A after lunch on evaluating Ruby without Ruby, and the other is the first session tomorrow about MRuby on small devices.
00:02:57.830
Since MRuby is compiled, unlike CRuby, how do you use it? The first step is to edit your buildconfig.rb. Unlike CRuby, where you can use any gem at any time, in MRuby you must specify your gem dependencies in advance in the build configuration.
00:03:09.579
You choose the compiler to use. MRuby CLI uses this for cross-compiling to different platforms. You can also specify library include paths in case you need to link with other C libraries.
00:03:25.650
If you're writing an MRuby gem, instead of embedding MRuby, you'll fill out an MRuby gem specification. Some MRuby CLI applications use the MRuby gem specification to specify the executable name and test files. The MRuby gem rake file, like RubyGems gem specifications, contains gem metadata, dependencies, and a list of binaries.
00:03:47.830
Metadata includes information like name, version, license, test files, and so on. You can also specify the gems you depend on and the binaries that get created. After you've configured these files, you run rake to build MRuby and all the executables you need. Once rake is complete, you'll see several executables in the bin directory.
00:04:07.340
If you are using MRuby CLI, the name of the command-line tool will appear in the view specified in the gem's specification. You will also have MRuby and mrb executables that have all the dependencies compiled in, which you can use to run one-off tests. If you enabled testing for this build, you'll have an MRuby test.
00:04:34.240
The goal of every MRuby CLI is to allow Rubyists to build single-file executables in a familiar environment. Last year, Terrence Lee and Zachary Scott gave a talk on MRuby CLI which showed the basics of the tool and how it works. The very basic description of MRuby CLI is that you can write Ruby and release software on Linux, OS X, and Windows.
00:05:01.310
The typical development loop using MRuby CLI begins with writing some MRuby code, compiling it, running tests, and returning to the start if the tests don't pass. Once you have enough features built and tests passing, you can release the executables.
00:05:19.900
When I first heard about MRuby and MRuby CLI, I thought it sounded easy, but I was certain there would be challenges when using a different Ruby implementation. The first challenge is that MRuby does not support everything CRuby does. For instance, MRuby's core library contains classes like String, Array, and Hash, but only supports a minimum number of methods compared to CRuby.
00:05:58.480
To access the full experience of CRuby, you need to add extension gems like MRuby Array Ext, which can be easily overlooked if you are developing your own MRuby gem. Forgetting to include these extension gem dependencies will lead to failures if the gem is used in another project.
00:06:21.700
Another issue is that MRuby has a smaller language outside the standard library; there are no predefined globals, such as load path, since everything is compiled in. Some libraries you're importing from CRuby may depend on these globals, which you would have to work around.
00:06:44.160
I encountered a problem while attempting to support documentation when porting a library from CRuby. I haven't reproduced it on its own yet, so a bug needs to be filed on it. While keyword arguments can be used to call methods in MRuby, you cannot define methods that automatically check your arguments like you can in CRuby.
00:07:08.360
If you haven't explored keyword arguments yet, there is a talk today in the second session after lunch in Ballroom C.
00:07:16.220
Also, some of the Gsub replacements don't work in MRuby, and regular expressions in MRuby seem less useful than in CRuby. I have found myself exploring the C source more to understand exceptions being raised, though I am not sure if this is due to unfamiliarity with MRuby or other reasons.
00:07:40.520
There are many pure Ruby libraries already written in CRuby that you might want to use in your MRuby projects. Overall, this process is straightforward, provided you remain aware of the restrictive syntax. Unfortunately, porting the tests for these libraries is much harder due to differences in how tests run, which we will cover later in this talk.
00:08:07.160
The existing gems for MRuby feel reminiscent of working with libraries from the Ruby 1.6 days. This could be because there haven't been enough contributors to develop libraries that are broadly applicable to different types of problems. As a result, an MRuby library often has a very narrow focus, which means you'll either have to hunt extensively to find a library that fits your use case or submit patches to get it working.
00:08:33.080
Personally, I have submitted patches for various libraries, with most being fairly small, which is a benefit. The last challenge I'll cover regarding MRuby involves extracting build information. Since MRuby is compiled, it needs to link with C libraries to compile successfully. MRuby CLI also requires cross-compiling these libraries for various platforms.
00:09:07.440
Unfortunately, extracting information from the build's MRuby build system is challenging without loading all of its rake files, which can be slow. I think enhancing this aspect could make MRuby's build system more flexible.
00:09:25.260
Working with MRuby CLI was a new experience for me, and it took time to learn the right way to use it. One of the first challenges was Docker. The MRuby CLI's build system is based on Docker, which offers the advantage of being ready to cross-compile for various systems without extra setup.
00:09:38.560
Unfortunately, Docker can be somewhat clumsy in how MRuby CLI operates; typically, Docker is used for long-lived services, while MRuby CLI only needs the container to run long enough to finish compiling or running tests. While Docker is arguably the best option available due to the minimal setup required for building these command-line tools, starting a build requires a lengthy command.
00:10:03.860
In situations where a compile problem arises, debugging can prove difficult because you have to first start a shell inside Docker, determine which commands were executed, and then run those commands to reproduce the issue before you can address it. Additionally, when you are in the Docker shell, your familiar development environment is not available, making it more challenging to perform fixes.
00:10:24.640
The most significant challenge I faced with MRuby CLI was its build system. Most of the issues I encountered stemmed from how the tasks were organized. The tasks for building your executable and those for building MRuby were all mixed together into a single rake command.
00:10:45.330
This made it challenging to add your tasks and have them run appropriately. For instance, before compiling the MRuby curl extension, the curl C library must be installed for every platform you are cross-compiling to, which is quite difficult by default.
00:11:10.680
Due to these complications, I made some improvements to the build tasks. The first was to build MRuby separately from the other work performed inside Docker, allowing me to cross-compile C libraries in advance.
00:11:22.830
The other improvement was to have a rake test that exists outside Docker, initiating Docker for me. This enhanced my capability to perform additional tasks that didn't require Docker, simplifying debugging.
00:11:38.820
The new build system runs rake three times. First, the outer rake tasks are executed, and once they are finished, the inner rake tasks run inside Docker. These inner tasks handle operations like cross-compilation or other setups that can only occur inside Docker.
00:12:00.100
Once the inner tasks are finished, MRuby's rake tasks build your command-line tool. Since the outer tasks don’t require Docker, you can utilize them for actions such as downloading or unpacking MRuby in preparation for compilation.
00:12:25.290
In addition, there are global test setup tasks that I will cover later, and finally, release tasks to publish your finished gem executable.
00:12:47.350
All the inner tasks run inside Docker, cross-compiling the C libraries you configured and subsequently invoking rake again to compile MRuby and conduct tests.
00:13:06.970
There are several additional benefits of my changes to the build system; it's now much easier to add hooks for custom tasks since tasks are well-defined for different phases of the build.
00:13:25.300
I also improved rake test performance by only compiling for the host platform when running tests, eliminating the need to build for all platforms when tests run only on the host platform.
00:13:40.100
It’s also quite likely that you will need to use C libraries as dependencies in your MRuby gem project. There are two options: you can statically link the libraries, embedding them inside your command-line tool and keeping a single file executable.
00:13:56.000
This simplifies installation since there are no additional instructions needed for users to install the relevant C libraries. However, the downside is that you will need to re-release your command if vulnerabilities in the C libraries arise, and this approach does increase the executable size.
00:14:14.640
The second approach is to dynamically link your C libraries. This has the advantage of easier vulnerability management, as the user is responsible for upgrading their libraries for any security concerns, resulting in less for you to manage.
00:14:34.230
A disadvantage of this option is that it adds an external dependency, making your executable no longer a single file. Another challenge I faced with MRuby CLI involved cross-compiling libraries.
00:14:52.480
MRuby CLI can cross-compile your executable for up to six different platforms, meaning the C libraries must also be compiled for all those platforms.
00:15:00.480
For example, for libcurl, you can't simply install the libcurl development package inside Docker and be done, as that library doesn't work on OS X or Windows.
00:15:24.970
Instead, you need to download the source, unpack it, cross-compile for each platform you'll be releasing on, and then configure MRuby's compiler to use those libraries.
00:15:47.190
To streamline this process, I wrote a tool for automating cross-compiling dependencies. You can configure the library to cross-compile by specifying a few values such as the name of the library, the release name for creating the source directory, and the URL from which the source will be downloaded. You can also set extra configuration flags.
00:16:06.300
In part two, I will discuss the origins and the structure of the command-line tool. My exploration of MRuby CLI began with a tool that our customer support team could use to configure a new product we are developing.
00:16:26.590
Our service was, and still is, API-only, and there is no web UI currently available for users. The service has a complicated setup, so I sought to automate common use cases to make it easier and reduce the potential for errors.
00:16:39.970
Moreover, I wanted to create a tool that customer support representatives could send to users, ultimately reducing their burden in supporting this new product.
00:16:59.230
To solve these challenges, I decided to write a tool that would automate setup, validate inputs, and minimize the number of errors users would encounter. It would also have the added benefit of providing design feedback on the API, allowing for further improvements before full exploration.
00:17:15.500
Before exploring MRuby CLI, I started with prototypes in CRuby. The first was a single-file script primarily serving as documentation. It was easy for me to write and allow customer support to modify it for their needs, but it wasn't flexible enough to handle all the ways they needed to interact with the API.
00:17:38.830
As customer support's needs grew, I split this single script into multiple scripts, each with its specific purpose—such as listing options, performing initial setups, or enabling/disabling features.
00:17:55.780
I focused on ensuring these scripts were well-documented to facilitate modifications for future scripts. Eventually, I extracted a couple of classes from these scripts for reuse, including a basic implementation of a JSON API and one for argument parsing using Ruby's option parser.
00:18:11.990
What lessons did I learn from these prototypes? First, documentation for the tool was extremely important. Since we had not budgeted any time for API documentation of the service, having documentation within the command-line tool proved very useful.
00:18:29.590
Roughly half the lines in the scripts ended up being comments, which allowed our support staff to understand what the tool was doing and suggest improvements beneficial to their workflow.
00:18:43.500
Keeping the scripts simple made everything more manageable. None of the scripts had external dependencies, allowing easy installation simply by checking out their repository. Additionally, most scripts were small, focusing on a single, well-defined purpose.
00:19:03.240
It was perfectly fine to have one or two larger scripts that automated key actions, such as initial setup. However, ultimately, these scripts became hard to maintain.
00:19:33.420
Our development environment relied on an older version of Ruby that did not support keyword arguments by default, necessitating the installation of a more modern version separately. Meanwhile, customer support would often add or edit existing tools to make their lives easier, resulting in multiple independent versions that were hard to support.
00:19:49.570
This led me to investigate a switch to MRuby CLI to alleviate the maintenance burden while striving to maintain high standards of maintainability.
00:20:05.360
I decided to model my command-line tool after the Ruby Gems pattern, utilizing a set of subcommands with a unique class for each while both leveraging option parser for argument parsing. Each subcommand features a common setup method to pick options for each command.
00:20:25.250
After parsing the arguments, the execute method runs the command and manages any errors that arise. For example, we have a command that lists conditions, requiring a service and version against which the conditions are associated.
00:20:50.620
The execute method then performs the necessary API operations to fetch the conditions from the service and display them. I decided to leverage a Ruby table-based approach from the CRuby standard library for option parsing.
00:21:07.220
I chose this for its familiarity among Rubyists and its good documentation. The option parser provides automatic type checking and argument completion, reducing the amount of code required to check these manually.
00:21:20.180
Moreover, it also offers shell completion by default, making parsing personal arguments easy. For API requests, I chose the MRuby curl library after evaluating several other HTTP libraries available for MRuby.
00:21:32.960
The MRuby curl library appears to be the most mature, primarily because it utilizes curl. It supports both HTTP, which we use in our development environment, and HTTPS for accessing production systems.
00:21:56.300
The library also supports persistent connections, which is especially important for HTTP requests. However, the downside is that, as a library dependency, it must be cross-compiled and linked into the tool.
00:22:25.640
There are two MRuby gems that have become quite popular: the MRuby Test gem for assertions and the MRuby Test gem used for running test cases. While using these gems is relatively simple, setting up the test cases can be a frustrating experience.
00:22:44.080
Testing presented me with the most challenges while building the command-line tool. It is crucial to writing maintainable software, but the unfamiliar environment required specific adjustments to understand testing within the MRuby CLI.
00:23:20.250
The MRuby Test gem is similar to the MiniTest library that ships with CRuby. You create a subclass of the test class and write test methods just as you would with MiniTest. Setup and teardown methods are used for common test setups.
00:23:37.700
However, you need to start tests using MRUBY_TEST_UNIT at the bottom of your test case, as MRuby lacks an exit method without an additional gem. Since MRuby is compiled, you cannot directly run the tests; instead, you rely on the MRuby Test gem to provide the test runner.
00:24:04.520
When tests run, a new MRuby VM is created for each test file, ensuring complete isolation between tests. The design of the test runner introduces complications. First, creating a global test setup requires setting test preload in your MRuby gem specification.
00:24:23.100
Without setting this, the helper won't be available to your tests since MRuby considers it a separate test file. I'm still searching for a solution to get this to work since I only discovered it recently.
00:24:42.140
The second complication involves Docker. MRuby CLI uses Docker Compose for test setups, but most documentation focuses on the Docker command, which caused some confusion in figuring out how to set up DNS records for tests.
00:25:05.300
Ideally, I would have preferred to mock interactions, but due to the lack of a straightforward test helper and mocking library, I opted to use the development services directly.
00:25:25.860
Additionally, the MRuby CLI's initial start-up is slow due to compiling all platforms beforehand, which significantly lengthens the feedback loop even for a minor change.
00:25:46.630
In response, I chose to perform some test setup before beginning any tests, utilizing the improved build structure to set everything up outside of Docker.
00:26:07.290
Given that I learned about test preload recently, I currently pass needed data for global setup into the tests using environment variables so that each test can retrieve them when needed.
00:26:27.250
Furthermore, I refined the rake tasks to only compile for the host platform used during tests, meaning I only had to compile a single platform instead of all seven.
00:26:48.750
MRuby CLI also supports bin tests, acting as integration tests, but since I prefer strong unit tests, I have yet to fully explore these.
00:27:05.740
In an integration test, you run your command and capture the output to validate its format. Despite these challenges, the question arises: why choose MRuby CLI over other tools like Go that offer similar capabilities?
00:27:29.460
I am primarily a Rubyist and work with a team of Rubyists. While we are open to learning new things, Ruby keeps us in our comfort zone. When I faced difficulties with the test setup, I could pivot back to CRuby, which was much more familiar for getting tasks done.
00:27:54.210
As I also want a tool I can deliver to Fastly customers, being able to work within my area of expertise simplifies the learning curve and reduces my stress, enhancing my ability to deliver quality software.
00:28:19.630
Now we approach the conclusion: what improvements would I like to see to make MRuby and MRuby CLI easier to use?
00:28:39.860
For MRuby CLI, I plan to contribute the improvements I made to the build system. I have had preliminary discussions with the maintainer about these enhancements and the benefits they could bring, and so far, they seem positive.
00:29:00.550
Additionally, the documentation for MRuby CLI could benefit from some improvements as it is not immediately clear where to start with a new command-line tool or what complications might arise.
00:29:25.290
There are libraries worth checking out to facilitate development, and I would also like to streamline the process for upgrading to newer versions of MRuby CLI, though I am not yet certain how that will work.
00:29:41.440
Having more accurate backtraces would be extremely helpful; currently, it's important to pay attention to pinpoint where MRuby indicated an error occurred. Moreover, I want to separate the loading of the build configuration from generating rake tasks, making it easier to unify MRuby's build system with MRuby CLI.
00:30:15.870
Lastly, it would be beneficial to have keyword arguments in methods for MRuby's C API as a fancy method of argument checking. I'm unsure how this will function with keyword arguments in CRuby, but I would like to see the mrb_get_args functionality ported over.
00:30:31.813
This function in the C API extracts method arguments when a Ruby method invokes a C function in MRuby, allowing for specifying argument types for automatic type checks and conversions.
00:30:55.250
The equivalent function in CRuby is rb_scan_args, but it is less expressive and only performs basic number validation, requiring manual type conversion.
00:31:12.540
I also found that figuring out the test preload was challenging; making this process automatic would simplify writing tests. Additionally, the MRuby test runner runs all tests without offering the option to execute a single test or a subset of tests.
00:31:29.340
Without this feature, making large modifications becomes significantly harder, as users must manually filter through tests to determine which test values are significant. Thank you for your time, and are there any questions?
00:31:46.520
Yes, the core question revolved around what is meant by passing data out-of-band for testing. Since you cannot pass data directly across Docker without involving file transfers, I set up the process to write a file containing required test setup data and load it within MRuby.
00:32:29.660
The next question was about the robustness of support for linking dynamic libraries. Yes, you can specify paths; the MRuby gem specification provides support for cross-compiling, allowing you to determine where the required libraries are located for different platforms.
00:32:45.440
For example, when I cross-compiled, the build would organize outputs into platform-specific directories, and I could direct it to find these libraries there. The next question was whether users can pass input if a library isn’t where expected.
00:33:09.180
I believe you would need to edit the buildconfig; I don't think overriding with CC, LD Flags, or library paths is directly possible. As for memory usage, I did not quantify the differences between CRuby and MRuby.
00:33:22.740
My CRuby prototype only runs briefly and performs minimal tasks, but I conducted a testing session of an MRuby curl patch that involved persistent connections, and the memory usage did not grow significantly.
00:33:39.080
As for the time it took to iterate from the first solution to the last, I was already familiar with building command-line tools in CRuby. The main challenge lay in understanding how to properly implement MRuby and the various pieces involved.
00:34:02.570
Writing the implementation of the command-line tool became straightforward once I'd grasped how to work with curl. Overall, I probably spent three to four days developing the overall implementation.
00:34:22.370
The next question was regarding the upgrade path for customers. How would I release a new version of the command-line tool? Since it is a single file, I can upload the binary directly and instruct users to download it.
00:34:39.340
Alternatively, if releasing on Linux, packaging it might be beneficial, especially when dynamic linking is involved. Ultimately, the release method would depend on how I'd like to distribute the product to end-users.
00:34:56.080
There was a question about notable command-line tools created with MRuby CLI. I recall a discussion at RubyKagi about Hakone, a tool for setting up containers. Unfortunately, I don’t have many more examples off the top of my head.
00:35:17.570
Terrence mentioned there's a wealth of tools in Japan, especially compared to the US. One notable example was a monitoring tool discussed in a RubyKagi talk. Lastly, there was an inquiry about how I cross-compiled libcurl.
00:35:41.230
The only steps needed were extracting the relevant compiler configurations provided by MRuby CLI and incorporating them into the configure script accompanying the libcurl source.
00:35:54.560
If you'd like more specific guidance on this topic, I can demonstrate it after the talk. Finally, a question arose regarding how MRuby differs from CRuby, and how both relate to Ruby.
00:36:07.490
CRuby is synonymous with MRI; syntactically, MRuby and CRuby are very similar, but there are some bugs present, and MRuby does not support every feature CRuby does. In particular, certain aspects may not align perfectly; for example, MRuby lacks automatic argument checking for keyword arguments.
00:36:25.830
Some global variables might also behave differently, leading to situations where certain libraries may not be compatible. I submitted a patch addressing an instance where a library did not expose optional captures properly.
00:36:43.140
In summary, while there are some gaps to be aware of, MRuby remains familiar to Rubyists to a large extent. Regarding the availability of require, you just list your MRuby gems in your specification, and they will be compiled in automatically.
00:36:57.370
Although some metaprogramming tricks you can leverage in CRuby may not apply in MRuby, as everything is compiled in and loaded at once, you don't need to worry about opportunistic loading. However, it does require a shift in your thinking concerning metaprogramming.
00:37:19.430
As for how many platforms I’ve built it for, I focus on accommodating Fastly's customer base across diverse operating systems. MRuby CLI has this capability built in.
00:37:34.570
Initially, I disabled Windows support to save time, but I can easily add support for other platforms later. Currently, I'm building for four platforms: Linux and OS X, both 32-bit and 64-bit.
00:37:54.650
There is no reason to disable support unless there is a specific testing need to focus on a particular platform. In general, MRuby CLI handles cross-compilation effectively.
00:38:10.000
Finally, concerning contributions to MRuby compared to CRuby, MRuby's internals are divided into multiple smaller gems. This structure allows for easier navigation and fixes for specific issues.
00:38:29.020
If you need to correct a bug in an array method, you can easily find where the change needs to be made, minimizing the complexities involved with contributing.
00:38:48.660
Terrence noted that MRuby is fully on GitHub, eliminating the hybrid Git-Subversion structure of CRuby. While the community size is smaller, it remains reasonably active.
00:39:05.510
At this point, I believe there are no further questions. Thank you all for your time and attention.