diff --git a/docs/collection.md b/docs/collection.md index ffebc9b832..45417080cb 100644 --- a/docs/collection.md +++ b/docs/collection.md @@ -2,6 +2,17 @@ `rbs collection` sub command manages third party gems' RBS. In short, it is `bundler` for RBS. +## Quickstart + +In a project that has a `Gemfile.lock`: + +```console +$ rbs collection init +$ rbs collection install +``` + +`rbs collection` reads `Gemfile.lock`, so there is no list of dependencies to maintain. `rbs` and type checkers such as Steep load the installed RBS files automatically. + ## Requirements * `git(1)` @@ -17,33 +28,44 @@ First, generate the configuration file, `rbs_collection.yaml`, with `rbs collect $ rbs collection init created: rbs_collection.yaml +rbs collection installs RBS files for the gems in your Gemfile.lock. +Gems in Gemfile.lock don't need to be listed anywhere. + +Next steps: + $ echo "/.gem_rbs_collection/" >> .gitignore + $ rbs collection install # writes rbs_collection.lock.yaml; keep it in version control + $ cat rbs_collection.yaml +# rbs collection installs RBS files for the gems in your Gemfile.lock. +# Run `rbs collection install` to resolve them into rbs_collection.lock.yaml and install them. + # Download sources sources: - - name: ruby/gem_rbs_collection + - type: git + name: ruby/gem_rbs_collection remote: https://github.com/ruby/gem_rbs_collection.git revision: main repo_dir: gems +# You can specify local directories as sources also. +# - type: local +# path: path/to/your/local/repository + # A directory to install the downloaded RBSs path: .gem_rbs_collection # gems: +# # RBS for a library that doesn't appear in Gemfile.lock, such as a non-gem standard library. +# - name: socket +# # # If you want to avoid installing rbs files for gems, you can specify them here. # - name: GEM_NAME # ignore: true ``` -I also recommend updating `.gitignore`. - -```console -$ echo /.gem_rbs_collection/ >> .gitignore -``` - ### Install dependencies Then, install gems' RBS with `rbs collection install`! It copies RBS from [the gem RBS repository](https://github.com/ruby/gem_rbs_collection) to `.gem_rbs_collection/` directory by default. -I recommend to ignore `.gem_rbs_collection/` from version control system, such as Git. ```console $ rbs collection install @@ -86,9 +108,9 @@ sources: path: .gem_rbs_collection gems: - # If the Gemfile.lock doesn't contain csv gem but you use csv gem, - # you can write the gem name explicitly to install RBS of the gem. - - name: csv + # If the Gemfile.lock doesn't contain socket but you use it, + # you can write the library name explicitly to install RBS of the library. + - name: socket # If the Gemfile.lock contains nokogiri gem but you don't want to use the RBS, # you can ignore the gem. diff --git a/lib/rbs/cli.rb b/lib/rbs/cli.rb index bf3a3c22af..c645c2deb2 100644 --- a/lib/rbs/cli.rb +++ b/lib/rbs/cli.rb @@ -1089,21 +1089,37 @@ def run_collection(args, options) case args[0] when 'install', 'instal', 'insta', 'inst', 'ins', 'in', 'i' - unless params[:frozen] - Collection::Config.generate_lockfile(config_path: config_path, definition: Bundler.definition) + if params[:frozen] + unless lock_path.exist? + if config_path.exist? + stderr.puts "#{lock_path} not found. Run `rbs collection install` without `--frozen` to generate it." + else + stderr.puts "#{lock_path} not found. Run `rbs collection init` and `rbs collection install` to generate it." + end + return 1 + end + else + return 1 unless collection_config_available?(config_path) + definition = bundler_definition or return 1 + Collection::Config.generate_lockfile(config_path: config_path, definition: definition) end Collection::Installer.new(lockfile_path: lock_path, stdout: stdout).install_from_lockfile when 'update', 'updat', 'upda', 'upd', 'up', 'u' + return 1 unless collection_config_available?(config_path) + definition = bundler_definition or return 1 # TODO: Be aware of argv to update only specified gem - Collection::Config.generate_lockfile(config_path: config_path, definition: Bundler.definition, with_lockfile: false) + Collection::Config.generate_lockfile(config_path: config_path, definition: definition, with_lockfile: false) Collection::Installer.new(lockfile_path: lock_path, stdout: stdout).install_from_lockfile when 'init' if config_path.exist? - puts "#{config_path} already exists" + stderr.puts "#{config_path} already exists" return 1 end config_path.write(<<~'YAML') + # rbs collection installs RBS files for the gems in your Gemfile.lock. + # Run `rbs collection install` to resolve them into rbs_collection.lock.yaml and install them. + # Download sources sources: - type: git @@ -1120,27 +1136,57 @@ def run_collection(args, options) path: .gem_rbs_collection # gems: + # # RBS for a library that doesn't appear in Gemfile.lock, such as a non-gem standard library. + # - name: socket + # # # If you want to avoid installing rbs files for gems, you can specify them here. # - name: GEM_NAME # ignore: true YAML - stdout.puts "created: #{config_path}" + stdout.puts <<~MESSAGE + created: #{config_path} + + rbs collection installs RBS files for the gems in your Gemfile.lock. + Gems in Gemfile.lock don't need to be listed anywhere. + + Next steps: + $ echo "/.gem_rbs_collection/" >> .gitignore + $ rbs collection install # writes rbs_collection.lock.yaml; keep it in version control + MESSAGE when 'clean' unless lock_path.exist? - puts "#{lock_path} should exist to clean" + stderr.puts "#{lock_path} should exist to clean" return 1 end Collection::Cleaner.new(lockfile_path: lock_path) when 'help', 'hel', 'he', 'h' - puts opts.help + stdout.puts opts.help else - puts opts.help + stdout.puts opts.help return 1 end 0 end + def collection_config_available?(config_path) + return true if config_path.exist? + + stderr.puts "#{config_path} not found. Run `rbs collection init` to generate it." + false + end + + def bundler_definition + definition = Bundler.definition + return definition if definition.lockfile.file? + + stderr.puts "#{definition.lockfile} not found. Run `bundle install` to generate it." + nil + rescue Bundler::GemfileNotFound + stderr.puts "Gemfile not found. `rbs collection` reads the gems to install from Gemfile.lock." + nil + end + def collection_options(args) OptionParser.new do |opts| opts.banner = <<~HELP diff --git a/sig/cli.rbs b/sig/cli.rbs index 6dcd5bfdfd..c631ed8216 100644 --- a/sig/cli.rbs +++ b/sig/cli.rbs @@ -76,6 +76,12 @@ module RBS def run_collection: (Array[String], LibraryOptions) -> Integer + # Returns `true` if the collection config exists, otherwise prints a message to `stderr` and returns `false` + def collection_config_available?: (Pathname) -> bool + + # Returns `nil` if `Gemfile` or `Gemfile.lock` is not found, after printing a message to `stderr` + def bundler_definition: () -> Bundler::Definition? + def run_annotate: (Array[String], top) -> Integer def run_subtract: (Array[String], top) -> Integer diff --git a/sig/shims/bundler.rbs b/sig/shims/bundler.rbs index d43bd595c6..e80ef39ec7 100644 --- a/sig/shims/bundler.rbs +++ b/sig/shims/bundler.rbs @@ -1,4 +1,7 @@ module Bundler + class GemfileNotFound < StandardError + end + class LockfileParser def initialize: (String) -> void diff --git a/test/rbs/cli_test.rb b/test/rbs/cli_test.rb index 3c70ffa203..808f7de410 100644 --- a/test/rbs/cli_test.rb +++ b/test/rbs/cli_test.rb @@ -1082,6 +1082,134 @@ def foo: () -> void end end + def test_collection_init + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + with_cli do |cli| + assert_cli_success do + cli.run(%w(collection init)) + end + + config = Pathname(dir).join(RBS::Collection::Config::PATH).read + assert_match(/gems in your Gemfile\.lock/, config) + assert_match(/rbs collection install/, config) + + yaml = YAML.load(config) + assert_equal ".gem_rbs_collection", yaml["path"] + assert_equal ["ruby/gem_rbs_collection"], yaml["sources"].map {|source| source["name"] } + + assert_match(/created: .*rbs_collection\.yaml/, stdout.string) + assert_match(/gems in your Gemfile\.lock/, stdout.string) + assert_match(/\$ rbs collection install/, stdout.string) + end + end + end + end + + def test_collection_install_without_config + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + with_cli do |cli| + refute_cli_success do + cli.run(%w(collection install)) + end + + assert_match(/rbs_collection\.yaml not found/, stderr.string) + assert_match(/rbs collection init/, stderr.string) + end + end + end + end + + def test_collection_update_without_config + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + with_cli do |cli| + refute_cli_success do + cli.run(%w(collection update)) + end + + assert_match(/rbs_collection\.yaml not found/, stderr.string) + assert_match(/rbs collection init/, stderr.string) + end + end + end + end + + def test_collection_install_without_gemfile + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + Pathname(dir).join(RBS::Collection::Config::PATH).write(<<~YAML) + sources: [] + path: .gem_rbs_collection + YAML + + _stdout, stderr = run_rbs_collection("install", bundler: false) do |status| + refute_predicate status, :success? + end + + assert_match(/Gemfile not found/, stderr) + end + end + end + + def test_collection_install_without_gemfile_lock + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + dir = Pathname(dir) + dir.join(RBS::Collection::Config::PATH).write(<<~YAML) + sources: [] + path: .gem_rbs_collection + YAML + dir.join("Gemfile").write(<<~RUBY) + source "https://rubygems.org" + RUBY + + _stdout, stderr = run_rbs_collection("install", bundler: false) do |status| + refute_predicate status, :success? + end + + assert_match(/Gemfile\.lock not found/, stderr) + assert_match(/bundle install/, stderr) + end + end + end + + def test_collection_install_frozen_without_lockfile + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + Pathname(dir).join(RBS::Collection::Config::PATH).write(<<~YAML) + sources: [] + path: .gem_rbs_collection + YAML + + with_cli do |cli| + refute_cli_success do + cli.run(%w(collection install --frozen)) + end + + assert_match(/rbs_collection\.lock\.yaml not found/, stderr.string) + assert_match(/without `--frozen`/, stderr.string) + end + end + end + end + + def test_collection_install_frozen_without_config_and_lockfile + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + with_cli do |cli| + refute_cli_success do + cli.run(%w(collection install --frozen)) + end + + assert_match(/rbs_collection\.lock\.yaml not found/, stderr.string) + assert_match(/rbs collection init/, stderr.string) + end + end + end + end + def test_collection_install omit_on_jruby! "`rbs collection install` runs `bundle install`, which builds native gem extensions that do not compile on JRuby"