Keygen is now Fair SourceStar us on GitHub arrow_right_alt

How to License and Distribute a Private Ruby Gem

Thursday, December 16th 2021

This blog post is outdated. We now offer an official Rubygems engine for distributing private Ruby gems. Please see the docs to get started. We're leaving this blog post up for posterity, because it may still be helpful for those wanting to use an S3-compatible service for distributing gems.


Earlier this week, we learned how to license and distribute private Docker images. Before that, we took a deep dive into Node packages, and Electron apps. Today, we'll be going over how to use Keygen to license and distribute a private Ruby gem.


If you're a Rubyist, you've probably heard of Sidekiq. Sidekiq is an open-source background job processing library, and I've never written a Rails code base without it. It's ubiquitous in the Ruby and Rails ecosystems.

But Sidekiq is not just an open-source library. It also has a paid version — and the creator of Sidekiq, Mike Perham, makes over a million dollars a year licensing the commercial closed-source "add-on" to Sidekiq called Sidekiq Pro. This type of business model, called an "open-core" licensing model, is becoming increasingly popular, and it seems to work well, both for users and for the maintainers.

What's an "open-core" licensing model? An open-core licensing model is when an open-source project also sells a "pro" version of the software. So the core product is "open", but has options for paid add-ons, which are typically closed-source. Alternatively, there is another model for monetizing open-source called dual licensing, where an open-source project is licensed for personal use, but commercial use requires a separate license.

Today, we're going to dive into how you can use Keygen to license a commercial Ruby gem of your own. (Into Node instead of Ruby? Check out this post.)

Building our Ruby gem

Like our other posts in this series, we'll be creating a simple hello Ruby gem.

To start out, let's create our gem's directory structure. This will follow best practices on building a gem.

mkdir -p lib/
 
touch lib/hello.rb

Then we'll add our Hello class to our hello.rb file.

class Hello
def self.world
puts "Hello, world!"
end
end

Next, we'll create our gem's gemspec.

touch hello.gemspec

And fill it with the following. (Of course, adjusting based on your actual gem.)

Gem::Specification.new do |s|
s.name = 'hello'
s.version = '1.0.0'
s.summary = 'Hello, world!'
s.description = 'A simple hello world gem'
s.authors = ['Zeke Gabrielse']
s.email = '[email protected]'
s.files = ['lib/hello.rb']
s.licenses = ['Nonstandard']
end

Lastly, we'll build our gem. This will pack up our Ruby gem into a .gem file. But before we do that, we'll want to create a new directory for these files. We'll be managing our private .gem files inside of a directory called gems/, itself inside of a build/ directory.

mkdir -p build/gems/

Next, we'll build our gemspec using the gem CLI.

gem build hello.gemspec --output=build/gems/hello-1.0.0.gem

Now that we've got a basic gem created, how do we go from gem to gem server? Well, we're not actually going to be running a full RubyGems server. But we will be using Keygen as a static private gem source.

Creating our gem source

Like I mentioned, we're going to be using Keygen's API as a static file store, very similar to how one would use AWS S3 to host a private RubyGem. The immediate benefit of using Keygen over something like S3, is that you're able assert that gems are only accessible to licensed users with a valid license for the product.

This makes common things, such as timed trials, much easier to manage compared to managing IAM policies in S3 for each customer, and revoking access after a trial license has expired or has otherwise been suspended. With Keygen, all of this is automatic.

For a static gem source (i.e. a gem source that isn't running a full RubyGems server), we'll need a handful of files. And we currently only have one of them, hello-1.0.0.gem.

To create the required files for a private gem source, we'll generate an "index", also using the gem CLI. The "index" is what bundler will use to request our gem from Keygen, making it a true gem source. An index can include more than 1 gem, as long as their .gem files are all in the gems/ directory at the time the index is generated.

gem generate_index --directory build/

Now, if we list out the generated files, we should see something like below.

find build/* -type f
# => build/gems/hello-1.0.0.gem
# build/latest_specs.4.8
# build/latest_specs.4.8.gz
# build/prerelease_specs.4.8
# build/prerelease_specs.4.8.gz
# build/quick/Marshal.4.8/hello-1.0.0.gemspec.rz
# build/specs.4.8
# build/specs.4.8.gz

Publishing our gem

Next, we're going to upload our files, or "artifacts", to Keygen's API. We'll use Keygen's new CLI for this, which makes publishing super easy. You could also use the Dashboard to create and upload each release artifact.

But first, we'll want to set a few environment variables, to save us some typing later on. (Alternatively, you can specify these using the --account, --product and --token flags, respectively.)

export KEYGEN_ACCOUNT_ID="<YOUR_KEYGEN_ACCOUNT_UUID>"
export KEYGEN_PRODUCT_ID="<YOUR_KEYGEN_PRODUCT_UUID>"
export KEYGEN_PRODUCT_TOKEN="<YOUR_KEYGEN_PRODUCT_API_TOKEN>"

After we've set those vars, we can use the keygen CLI to publish our files. Using the dist command, we'll create a release for each file in the build/ directory.

# Create a new release
keygen new --version '1.0.0'
 
# Publish our hello gem
keygen upload build/gems/hello-1.0.0.gem \
--filename 'gems/hello-1.0.0.gem' \
--release '1.0.0'
 
# Publish various specs for our gem source
keygen upload build/latest_specs.4.8 --filetype '' --release '1.0.0'
keygen upload build/latest_specs.4.8.gz --release '1.0.0'
keygen upload build/prerelease_specs.4.8 --filetype '' --release '1.0.0'
keygen upload build/prerelease_specs.4.8.gz --release '1.0.0'
keygen upload build/specs.4.8 --filetype '' --release '1.0.0'
keygen upload build/specs.4.8.gz --release '1.0.0'
 
# Publish the quick gem spec
keygen upload build/quick/Marshal.4.8/hello-1.0.0.gemspec.rz \
--filename 'quick/Marshal.4.8/hello-1.0.0.gemspec.rz' \
--release '1.0.0'
 
# Publish the new release
keygen publish --release '1.0.0'
 
# Untag our previous release if it exists (feel free to use a different tag e.g. 'latest')
keygen untag gems --release 'gems'
 
# Tag our new release
keygen tag gems --release '1.0.0'

By default, the CLI uses the file's basename() as an artifact's filename (which excludes any directories), so we're providing the --filename flag so that our artifacts have the full filepath e.g. any files under the gems/ and quick/ directories.

To publish a new version of your gem, you'd build the new version's .gem file, and then you'd want to regenerate the index.

gem generate_index --directory build/ --update

And finally, re-upload the index files.

Installing our gem

Now that we've gotten our gem uploaded... how do we use it? Well, pretty much like any other gem! Simply provide a license token in the source URL's auth part and Keygen will automatically assert the token belongs to a valid license for the product.

For example, see this Gemfile:

auth = 'license:C1B6DE-39A6E3-DE1529-8559A0-4AF593-V3'
 
source "https://#{auth}@api.keygen.sh/v1/accounts/demo/artifacts" do
gem 'hello', '~> 1.0'
end

Which can then be executed like this,

require 'hello'
 
Hello.world
# => Hello, world!

You can also install it like so,

gem install hello -v '1.0.0' \
--source 'https://license:[email protected]/v1/accounts/demo/artifacts'

In conclusion

Today we've covered how to create and build a simple Ruby gem. We then learned how we can use gem generate_index to generate a set of files that can be hosted on Keygen, or another service such as S3, turning it into a private Ruby gem source.

Pack all of this up into a Rakefile and publishing a new gem release could be as simple as invoking a single Rake task. In fact, somebody did just that!

We're excited to see what people build.

Until next time.


If you find any errors in my code, or if you can think of ways to improve things, ping me via Twitter.