Introduction

Role models are important.

— Officer Alex J. Murphy / RoboCop

This Packaging style guide outlines the recommended best practices for real-world programmers to write code that can be maintained both, upstream and downstream.

You can generate a PDF copy of this guide using AsciiDoctor PDF, and an HTML copy with AsciiDoctor using the following commands:

# Generates README.pdf
asciidoctor-pdf -a allow-uri-read README.adoc

# Generates README.html
asciidoctor README.adoc
Tip

Install the rouge gem to get nice syntax highlighting in the generated document.

gem install rouge
Tip

RuboCop, a static code analyzer (linter) and formatter, has a rubocop-packaging extension, based on this style guide.

How To Read This Guide

The guide is separated into sections based on the different cops that the Packaging extension provides. There was an attempt to explicitly mention everything so if anything is still unclear, feel free to open an issue asking for further clarity.

A Living Document

This guide is a work in progress - existing guidelines are constantly being improved, new guidelines are being added, and occasionally some guidelines would get removed.

Why Packaging Extension?

The Debian Ruby team has a lot of experience in packaging and maintaining Ruby libraries and applications for Debian. During this work, they identified several issues in upstream codebases that make it difficult to build a Debian package straight out of those Ruby gems (shipped via rubygems).

The Debian developers (downstream maintainers) have been in touch with the RubyGems and other upstream maintainers and we’re collaborating to try to make things easier for OS packagers while not compromising the experience for upstream maintainers.

As a result, we’ve come up with this style guide, mentioning the best practices that upstream gem and project maintainers can follow to make the lives of packagers easier.

Source Code Layout

Using git in gemspec

Avoid using git ls-files to produce lists of files. Downstreams (OS packagers) often need to build your package in an environment that does not have git (on purpose). Instead, use some pure Ruby alternatives, like Dir or Dir.glob.

Rationale

Packages in Debian are built in a clean environment (sbuild, schroot, et al) and whilst doing so, the build fails with:
Invalid gemspec in [<gem_name>.gemspec]: No such file or directory - git

And adding git as a dependency for each of the Ruby packaging is something that is not right and definitely not recommended. Besides, the source package consists of released tarballs (usually downloaded from GitHub/GitLab releases page or converted from the .gem file, obtained using gem fetch foo), which is extracted during build. So even if we add git as a build dependency, it would still fail as the Debian package source tree is not a git repository. Even when the package is maintained in git, it is uploaded as tarballs to the archive without any version control information.

Therefore, the best way forward here is to patch out the usage of git and use some plain Ruby alternatives like Dir or Dir.glob or even Rake::FileList whilst doing the Debian maintenance.

There’s not only Debian or other OS packaging situation/examples, but also a couple of others, for instance:

  • ruby-core as part of their CI system runs their test suite against an unpackaged Ruby tarball which doesn’t have a .git directory. That means they needed to overwrite the bundler gemspec to not use git.
    Actually, not anymore, since git has been removed from bundler’s gemspec.

  • If you build your application on a bare docker image without git, and you are pointing to a git sourced gem that uses git on its gemspec, you’ll get: No such file or directory - git ls-files (Errno::ENOENT) warnings all around. For example, if you use this in your Gemfile:
    gem "foo", git: "https://github.com/has-git-in-gemspec/foo"

Originally, git ls-files inside the default gemspec template was designed so that users publishing their first gem wouldn’t unintentionally publish artifacts to it. Recent versions of bundler won’t let you release if you have uncommitted files in your working directory, so that risk is lower.

# bad
Gem::Specification.new do |spec|
  spec.files         = `git ls-files`.split("\n")
  spec.test_files    = `git ls-files -- spec`.split("\n")
end

# good
Gem::Specification.new do |spec|
  spec.files         = Dir["lib/**/*", "LICENSE", "README.md"]
  spec.test_files    = Dir["spec/**/*"]
end

# bad
Gem::Specification.new do |spec|
  spec.files = Dir.chdir(File.expand_path(__dir__)) do
    `git ls-files -z`.split("\\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
  end
end

# good
require "rake/file_list"

Gem::Specification.new do |spec|
  spec.files         = Rake::FileList["**/*"].exclude(*File.read(".gitignore").split)
end

# bad
Gem::Specification.new do |spec|
  spec.files         = `git ls-files -- lib/`.split("\n")
  spec.test_files    = `git ls-files -- test/{functional,unit}/*`.split("\n")
  spec.executables   = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
end

# good
Gem::Specification.new do |spec|
  spec.files         = Dir.glob("lib/**/*")
  spec.test_files    = Dir.glob("test/{functional,test}/*")
  spec.executables   = Dir.glob("bin/*").map{ |f| File.basename(f) }
end

Using require_relative from test code into lib/

Avoid using require_relative with relative path from your test code (spec/ or tests/) to lib/. Use require instead.

Rationale

Debian has a testing infrastructure that is designed to test packages in their installed form, i.e., closer to how an end-user would use it than to how a developer working against it. For this to work, the test-suite must load code that’s installed system-wide, instead of the code in the source tree. Using require_relative from the tests into the lib directory makes that impossible, but it also makes the test look less like client-code that would use the code in your gem. Therefore, we recommend that test code uses the main library code without require_relative.

Therefore, when one uses a relative path, we end up getting a LoadError, stating:
cannot load such file — /[PKGBUILDDIR]/foo.

We want to emphasize that there is nothing wrong with using require_relative inside lib/, it’s just using it from your test code to the lib directory prevents the "test the library installed system-wide" use case.

Therefore, it is still recommended to use require_relative with just this exception to it.

# bad
require_relative "lib/foo"

# good
require "foo"

# bad
require_relative "../../lib/foo/bar"

# good
require "foo/bar"

# good
require_relative "foo/bar/bax"
require_relative "baz/qux"

Contributing

The guide is still a work in progress - new cops are being added, existing ones are being refactored. And whilst doing so, some bits might need a little more polishing and paraphrasing to make things clearer. Improving such guidelines is a great (and simple way) to help the Ruby community!

Also, nothing written in this guide is set in stone. We desire to work together with everyone interested in Ruby coding style, so that we could ultimately create a resource that will be beneficial to the entire Ruby community!

Feel free to open issues or send pull requests with improvements. Thanks in advance for your help!

How to Contribute?

It’s easy, just follow the contribution guidelines below:

License

Creative Commons License This work is licensed under a Creative Commons Attribution 3.0 Unported License.

Spread the Word

A community-driven style guide is of little use to a community that doesn’t know about its existence. Tweet/toot about the guide, share it with your friends and colleagues. Every comment, suggestion, or opinion we get, makes the guide a little bit better. And we want to have the best possible guide, don’t we?

Credits

This guide has been put together with the help of our experienced Ruby team in Debian. So a huge thanks to all of them and their work. Particularly, Antonio Terceiro, who has helped a lot in putting all this together in the best possible way!

And also a huge thanks to David Rodríguez, an upstream maintainer of Bundler and RubyGems, for collaborating on this and extending his help to try to make things easier for OS packagers while not compromising the experience for upstream maintainers (which is very important!).