[neovim][ruby] how to install gem that neovim ruby scripts can find?

I used home-manager to install neovim from github (master branch for version 0.5.x). I’m running Arch Linux as the base OS.

home.nix (click to expand)
{ config, pkgs, ... }:

let
  custom.neovim = pkgs.neovim-unwrapped.overrideAttrs (oa: {
    version = "master";
    src = builtins.fetchTarball {
      url = "https://github.com/neovim/neovim/archive/390fed3248ff8cd67849891bbf1f214cf17bb952.tar.gz";
      sha256 = "09g9655mw0m9wj3k924jpk661gli3m24qzvh3j9j6j7x8lkc5ch2";
    };

    buildInputs = oa.buildInputs ++ (with pkgs; [
      tree-sitter
    ]);

    cmakeFlags = oa.cmakeFlags ++ [
      "-DUSE_BUNDLED=OFF"
    ];
  });

in
{
  programs.home-manager.enable = true;

  home.username = "boxofrox";
  home.homeDirectory = "/home/boxofrox";

  home.stateVersion = "20.09";

  home.packages = with pkgs; [];

  programs.neovim = {
    enable = true;

    package = custom.neovim;

    withRuby = true;

    extraPackages = with pkgs; [
      neovim-remote
    ];

    extraConfig = ''
      call plug#begin('~/.config/nvim/plugged')

      Plug '~/files/development/vim-timecard'

      call plug#end()
    '';
  };
}

I wrote a ruby plugin for neovim (vim-timecard) to document the time I spend on tasks, and this plugin uses the hamster ~>2.0 gem for immutable data structures. I cannot figure out how to install hamster, such that, when I start nvim, my plugin doesn’t complain that it is unable to load hamster.

Any assistance would be appreciated.

I find it odd that the makeNeovimConfig [1] in nixpkgs allows me to specify python packages (extraPython3Packages) that are required, but doesn’t offer the same for ruby. ?_?

Things I’ve tried.

Install hamster using system’s default gem binary

I have the ruby v3.0.0 package provided by Arch Linux installed. It configures the gem tool to install gems under ~/.gem. I ran…

$ gem install hamster

… and promptly installed hamster 3.0.

vim-timecard cannot find hamster when neovim starts.

I can see hamster 3.0.0 in the gem list output.

Install hamster 2.0 using system’s default gem binary

I ran gem install 'hamster:~>2.0'.

gem list reports both 3.0 and 2.0 versions of hamster (no idea how that works when my ruby plugin cannot import libraries by version, to my knowledge).

nvim starts and vim-timecard cannot load hamster.

Review neovim ruby environment.

Start neovim. Run :ruby print ENV['PATH'].

/nix/store/yw25krsaa2kckpf02xcg96r0hs5bwxrz-neovim-ruby-env/lib/ruby/gems/2.7.0/bin
:/home/boxofrox/.nix-profile/bin
:/usr/bin
:/nix/store/yw25krsaa2kckpf02xcg96r0hs5bwxrz-neovim-ruby-env/bin
:/nix/store/1nacslwmmvym8pb3pngfd3lqpcyx9gsp-neovim-remote-2.4.0/bin

Then run :! /nix/store/yw25krsaa2kckpf02xcg96r0hs5bwxrz-neovim-ruby-env/bin/gem env.

gem env output (click to expand)
RubyGems Environment:
  - RUBYGEMS VERSION: 3.2.16
  - RUBY VERSION: 2.7.3 (2021-04-05 patchlevel 183) [x86_64-linux]
  - INSTALLATION DIRECTORY: /nix/store/yw25krsaa2kckpf02xcg96r0hs5bwxrz-neovim-ruby-env/lib/ruby/gems/2.7.0
  - USER INSTALLATION DIRECTORY: /home/boxofrox/.gem/ruby/2.7.0
  - RUBY EXECUTABLE: /nix/store/pdbpgvsq90hq0c0fkz5xz01bczr74aqx-ruby-2.7.3/bin/ruby
  - GIT EXECUTABLE: /usr/bin/git
  - EXECUTABLE DIRECTORY: /nix/store/yw25krsaa2kckpf02xcg96r0hs5bwxrz-neovim-ruby-env/lib/ruby/gems/2.7.0/bin
  - SPEC CACHE DIRECTORY: /home/boxofrox/.gem/specs
  - SYSTEM CONFIGURATION DIRECTORY: /nix/store/pdbpgvsq90hq0c0fkz5xz01bczr74aqx-ruby-2.7.3/etc
  - RUBYGEMS PLATFORMS:
     - ruby
     - x86_64-linux
  - GEM PATHS:
     - /nix/store/yw25krsaa2kckpf02xcg96r0hs5bwxrz-neovim-ruby-env/lib/ruby/gems/2.7.0
     - /home/boxofrox/.gem/ruby/2.7.0
     - /nix/store/pdbpgvsq90hq0c0fkz5xz01bczr74aqx-ruby-2.7.3/lib/ruby/gems/2.7.0
  - GEM CONFIGURATION:
     - :update_sources => true
     - :verbose => true
     - :backtrace => false
     - :bulk_threshold => 1000
  - REMOTE SOURCES:
     - https://rubygems.org/
  - SHELL PATH:
     - /home/boxofrox/.nix-profile/bin
     - /home/boxofrox/.nix-profile/bin
     - /home/boxofrox/.nix-profile/bin
     - /usr/bin
     - /nix/store/yw25krsaa2kckpf02xcg96r0hs5bwxrz-neovim-ruby-env/bin
     - /nix/store/1nacslwmmvym8pb3pngfd3lqpcyx9gsp-neovim-remote-2.4.0/bin

Ok. Seems the gem needs to install under ~/.gem/ruby/2.7.0, instead of ~/.gem/ruby/3.0.0.

Install hamster gem using neovim-ruby-env

Install gem with: /nix/store/yw25krsaa2kckpf02xcg96r0hs5bwxrz-neovim-ruby-env/bin/gem install 'hamster:~>2.0'

Verify gem installed with: /nix/store/yw25krsaa2kckpf02xcg96r0hs5bwxrz-neovim-ruby-env/bin/gem list. Reports hamster 2.0.0 is installed as a local gem.

Run nvim, and vim-timecard cannot load hamster.

Other notes

In neovim, :ruby print ENV.keys reports that GEM_HOME is defined, but not GEM_PATH. GEM_HOME points to a single path in the nix store (presumably created by the bundlerEnv in the neovim package file). I can only assume at this point that the neovim-ruby-env is not looking in ~/.gem/ for locally installed gems.

[1]: https://github.com/NixOS/nixpkgs/blob/3b6c3bee9174dfe56fd0e586449457467abe7116/pkgs/applications/editors/neovim/utils.nix#L21

I think it’s just because no one has shown enough interest in ruby plugins yet.
I am not familiar with ruby but if it doesn’t show expected GEM_PATH it’s maybe because neovim-ruby-env unsets it ? nix resorts to all sorts of wrapper so you could cat your gem binary to check how they set ccertain variables

gem looks to be a ruby script. ruby itself is an unwrapped executable. Maybe I should set my own GEM_PATH env var before running neovim. Dunno why that didn’t occur to me.

$ cat /nix/store/yw25krsaa2kckpf02xcg96r0hs5bwxrz-neovim-ruby-env/bin/gem
#!/nix/store/pdbpgvsq90hq0c0fkz5xz01bczr74aqx-ruby-2.7.3/bin/ruby
#--
# Copyright 2006 by Chad Fowler, Rich Kilmer, Jim Weirich and others.
# All rights reserved.
# See LICENSE.txt for permissions.
#++

require 'rubygems'
require 'rubygems/gem_runner'
require 'rubygems/exceptions'

required_version = Gem::Requirement.new ">= 1.8.7"

unless required_version.satisfied_by? Gem.ruby_version then
  abort "Expected Ruby Version #{required_version}, is #{Gem.ruby_version}"
end

args = ARGV.clone

begin
  Gem::GemRunner.new.run args
rescue Gem::SystemExitException => e
  exit e.exit_code
end

So I tried running…

$ GEM_PATH=$HOME/.gem/ruby/2.7.0 nvim

…then…

:ruby print ENV['GEM_PATH']

…and the environment variable appeared in the message log. However, my plugin still cannot load hamster, even though it exists under $HOME/.gem/ruby/2.7.0/gems/hamster-2.0.0.

So workaround isn’t as simple as setting GEM_PATH, but this at least confirms that the neovim-ruby-env doesn’t unset the variable.

After a bit more testing, this situation appears to be more bonkers than before.

Test capabilities of neovim-ruby-env outside of neovim

Let’s run…

$ /nix/store/yw25krsaa2kckpf02xcg96r0hs5bwxrz-neovim-ruby-env/bin/ruby
require 'hamster'
print Hamster.constants
print '\n'
^D
# Output
[:List, :EmptySet, :Splitter, :EmptyHash, :Set, :Immutable, :Associable, :SortedSet, :VERSION, :EmptyVector, :Deque, :EmptyTrie, :Enumerable, :Trie, :Undefined, :Hash, :EmptyDeque, :Vector, :Partitioner, :Partitioned, :MutableHash, :ReadCopyUpdate, :EmptySortedSet, :Realizable, :LazyList, :Cons, :EmptyList]

So, even without GEM_PATH, neovim-ruby-env can load hamster…

Identify exactly which ruby binary runs a ruby script in neovim

Inside neovim, I run the ex command :ruby print Process.argv0 and get… drumroll

/nix/store/bzcmi1zplqbqlv6xnply6mb3n56bzh1r-neovim-master/bin/nvim-ruby

A repeat of the capability test from before yields…

$ /nix/store/bzcmi1zplqbqlv6xnply6mb3n56bzh1r-neovim-master/bin/nvim-ruby
Can't run neovim-ruby-host interactively.

Doh. Well the file is a ruby script, let’s see what it does.

$ cat /nix/store/bzcmi1zplqbqlv6xnply6mb3n56bzh1r-neovim-master/bin/nvim-ruby
#!/nix/store/pdbpgvsq90hq0c0fkz5xz01bczr74aqx-ruby-2.7.3/bin/ruby
#
# This file was generated by Nix.
#
# The application 'neovim-ruby-host' is installed as part of a gem, and
# this file is here to facilitate running it.
#

ENV["BUNDLE_GEMFILE"] = "/nix/store/iijk6j15j3k3nrr52bbr477xcfzqhfv5-gemfile-and-lockfile/Gemfile"
ENV.delete 'BUNDLE_PATH'
ENV['BUNDLE_FROZEN'] = '1'

Gem.paths = { 'GEM_HOME' => "/nix/store/yw25krsaa2kckpf02xcg96r0hs5bwxrz-neovim-ruby-env/lib/ruby/gems/2.7.0" }

$LOAD_PATH.unshift "/nix/store/pnqvrnhg4hcfkmrq06lhqp5f2l2v3y5b-bundler-2.1.4/lib/ruby/gems/2.7.0/gems/bundler-2.1.4/lib"

require 'bundler'
Bundler.setup()

load Gem.bin_path("neovim", "neovim-ruby-host")

This bit is over my head. I know bundler and gem are used to manage ruby libraries, but I can’t say whether this affects where neovim-ruby-host searches for gems, or even whether to ignore GEM_PATH.

That $LOAD_PATH looks interesting though.

Can I update the gem path inside the neovim ruby environment?

Yes I can.

$ nvim
:ruby $LOAD_PATH.unshift("/home/boxofrox/gem/ruby/2.7.0/hamster-2.0.0/lib")
:ruby $LOAD_PATH.unshift("/home/boxofrox/gem/ruby/2.7.0/concurrent-ruby-0.9.2/lib")
:ruby require 'hamster'
:ruby print Hamster::Vector["it works \o/"]
#<Hamster::Vector:0x0000000002b379d0>

I have yet to find a better (decoupled) way to manipulate the gem path for neovim ruby host. But… this isn’t that bad and seems preferrable.

To use this work-around, I can update $LOAD_PATH at the beginning of my plugin to include the two gems.

Why this might be preferred is this. I’m very dissatisfied with ruby plugins I wrote for vim when they depend on external gems because updates to my ArchLinux system would break the plugin when a new ruby version was installed, and the gems had to be reinstalled. It’s a PITA. At least with this work around, I can install the gems near my plugin, have the plugin add the gems to the path, and it works, and hopefully ruby version updates don’t break this setup as often.

Is there a better way?

Possibly. I imagine I should create a nix package for my vim plugin and install the plugin that way instead of with VimPlug. I haven’t stumbled across the secret nix expression that allows my ruby vim plugin to specify gem requirements and update the gem path of the neovim ruby host.

2 Likes