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.
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!" endend
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.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 releasekeygen new --version '1.0.0' # Publish our hello gemkeygen 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 sourcekeygen 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 speckeygen 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 releasekeygen 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 releasekeygen 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' \
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.