$30 off During Our Annual Pro Sale. View Details »

A Ruby version manager written in Rust, which is 7 seconds faster than rbenv

TaKO8Ki
September 18, 2021

A Ruby version manager written in Rust, which is 7 seconds faster than rbenv

Link to a slide on Canva

frum: https://github.com/TaKO8Ki/frum
gobang: https://github.com/TaKO8Ki/gobang
Article about frum: https://zenn.dev/tako8ki/articles/2021-09-ruby-version-manager-written-in-rust

I built a Ruby version manager with Rust, which is 7 seconds faster than rbenv. In detail, it takes about 7 seconds faster to install a particular version and set it with frum than rbenv.

Ruby has a lot of code that depends on the runtime version. Therefore, it is important to manage Ruby versions. However, it takes 2 to 3 minutes to run rbenv install, depending on the environment, and waiting for this to happen every time you install a new version is inefficient. The main bottleneck is a command-line utility called ruby-build, which makes it easy to install virtually any version of Ruby. In order to solve this problem, I created a Pure Rust Ruby version manager called frum.

In this talk, I will share my efforts to build a simple and easy to use Ruby version manager with Rust, and what I learned in the process of making frum like the following things.

- Tips on good E2E testing a CLI that manipulates files using declarative macros
- Tips for writing a CLI with multiple subcommands in Rust
- How to use the command line parser clap to implement flexible command-line completion

Q&A

frumはプロダクションでも利用できますか?例えば、今後どのくらいの期間保守が続けられる予定でしょうか?(追記)例えば、moneyforward社で利用予定はありますか?

> frumはプロダクションでも利用できますか?例えば、今後どのくらいの期間保守が続けられる予定でしょうか?

プロダクションでの利用はあまり想定していません。例えば、tempdirを利用しているので、ユーザー間でそれが競合してしまうことなどが考えられます。基本的に僕が仕事でRubyを書いている間はメンテする予定なので、少なくとも2、3年は問題ないと思います。基本的にはユーザーがいる限りメンテし続けようとは考えています。

> 例えば、moneyforward社で利用予定はありますか?

基本的に本番環境でバージョンマネージャーを用いたバージョンの切り替えなどは行ってないはずなので、本番環境での利用はしないと思われます。ローカルで使ってくださる方は、もしかしたらいるかもしれないですね。そこら辺は他のOSSと同じで個々の判断で使っていただく形になると思います。

TaKO8Ki

September 18, 2021
Tweet

More Decks by TaKO8Ki

Other Decks in Programming

Transcript

  1. A Ruby version manager written in
    Rust, which is 7 seconds faster than
    rbenv
    Takayuki Maeda (@TaKO8Ki)
    Rust.Tokyo 2021

    View Slide

  2. About myself

    View Slide

  3. About myself
    Takayuki Maeda
    GitHub: @TaKO8Ki
    Twitter: @TaKOBKi
    Software Engineer @ Money Forward, Inc.
    Senior at Kobe University majoring Civil Engineering

    View Slide

  4. Sometimes contribute to rust-clippy

    View Slide

  5. I've been building TUI SQL client with Rust
    lately.
    https://github.com/TaKO8Ki/gobang

    View Slide

  6. View Slide

  7. What are rbenv and frum?

    View Slide

  8. $ rbenv install 2.6.5 # Install a Ruby version using
    ruby-build
    $ rbenv local 2.6.5 # Set or show the local
    application-specific Ruby version
    $ rbenv global 2.6.5 # Set or show the global Ruby
    version
    What is rbenv?
    rbenv is a tool that lets you install and run multiple versions of
    Ruby side-by-side.

    View Slide

  9. What is ruby-build?
    ruby-build is a command-line utility that makes it easy to install
    virtually any version of Ruby, from source.

    View Slide

  10. What is frum?
    A little bit fast and modern Ruby version manager written in Rust
    Pure Rust implementation not using ruby-build
    Works with .ruby-version files
    Faster than rbenv
    github.com/TaKO8Ki/frum

    View Slide

  11. Motivations

    View Slide

  12. There are many products written in Ruby at
    Money Forward

    View Slide

  13. You need to wait for 2 to 3 minutes
    every time you install new versions of
    Ruby

    View Slide

  14. This is not cool

    View Slide

  15. Of course. There is a limit to how
    much faster I can make installing
    Ruby

    View Slide

  16. I feel like I can make it about 10
    seconds faster

    View Slide

  17. Benchmark

    View Slide

  18. Benchmark
    eval "$(rbenv init -)" → rbenv install → rbenv local
    vs
    eval "$(frum init)" → frum install → frum local

    View Slide

  19. Benchmark
    frum init vs rbenv init

    View Slide

  20. What did I improve?

    View Slide

  21. What did I improve?
    rbenv is written in bash script
    ruby-build is also written in bash script
    rbenv uses shims to switch Ruby versions

    View Slide

  22. ・rbenv is written in bash script
    ・ruby-build is also written in bash script
    ・rbenv uses shims to switch Ruby versions

    View Slide

  23. rbenv/libexec

    View Slide

  24. ・rbenv is written in bash script
    ・ruby-build is also written in bash script
    ・rbenv uses shims to switch Ruby versions

    View Slide

  25. ruby-build/bin
    ruby-build has 1468 lines

    View Slide

  26. So do I need to rewrite all of them in
    Rust?

    View Slide

  27. No. It's not necessary.

    View Slide

  28. $ ./configure
    $ make
    $ sudo make install
    What to do basically is
    Download source and unpack a tarball, then just do this:

    View Slide

  29. ./configure
    ⬅︎
    make
    ⬅︎

    View Slide

  30. make
    ⬅︎
    make install
    ⬅︎

    View Slide

  31. ・rbenv is written in bash script
    ・ruby-build is also written in bash script
    ・rbenv uses shims to switch Ruby versions

    View Slide

  32. What is shim?
    > In computer programming, a shim is a small
    library that transparently intercepts API calls and
    changes the arguments passed, handles the
    operation itself, or redirects the operation
    elsewhere.
    https://en.wikipedia.org/wiki/Shim_(computing)

    View Slide

  33. shims in ~/.rbenv/shims

    View Slide

  34. View Slide

  35. store the command name into $program
    ⬅︎

    View Slide

  36. store the command name into $program
    ⬅︎
    ⬅︎ setup $RBENV_DIR

    View Slide

  37. store the command name into $program
    ⬅︎
    ⬅︎ setup $RBENV_DIR
    ⬅︎
    setup $RBENV_ROOT
    rbenv exec

    View Slide

  38. In case you use Ruby 2.6.5
    rbenv exec ruby -v turns into
    PATH=”~/.rbenv/versions/2.6.5/bin:$PATH”
    ruby -v

    View Slide

  39. When does rbenv create shims?
    rbenv rehash creates shims. rbenv init runs it internally.

    View Slide

  40. When does rbenv create shims?
    So what does rbenv rehash do?
    rbenv rehash creates shims. rbenv init runs it internally.

    View Slide

  41. 1. rbenv rehash checks if the
    directory ~/.rbenv/shims exists

    View Slide

  42. 2. Rehash checks if
    ~/.rbenv/shims/.rbenv-shim exists

    View Slide

  43. 3. Rehash creates
    ~/.rbenv/shims/.rbenv-shim and get
    a lock on it

    View Slide

  44. View Slide

  45. 4. Rehash iterates through all files in
    ~/.rbenv/versions/${version}/bin/ of
    all Ruby versions installed. Then, it
    copies prototype file into the shim
    scripts

    View Slide

  46. 5. Eventually, rehash deletes the
    prototype file

    View Slide

  47. rbenv runs these steps every time you
    execute rbenv init

    View Slide

  48. How does frum solve this problem?
    frum uses symlinks to switch Ruby versions.
    src/commands/init.rs

    View Slide

  49. ⬅︎ generate temp dir
    src/commands/init.rs

    View Slide

  50. ⬅︎ generate temp dir
    src/commands/init.rs
    ⬅︎
    create a symlink to a Ruby directory

    View Slide

  51. In short, frum init simply creates a
    symlink

    View Slide

  52. That results in

    View Slide

  53. Tips

    View Slide

  54. Tips for good E2E testing
    Since frum has some subcommands, It is important to
    check if your state is as expected after running multiple
    commands.
    frum init frum install frum local
    ⬅︎
    ⬅︎

    View Slide

  55. Tips for good E2E testing
    e2e_test macro for E2E testing

    View Slide

  56. Tips for good E2E testing
    ⬅︎ define a test function
    ⬅︎
    setup

    View Slide

  57. Tips for good E2E testing
    utils::setup does the following things:
    prepares a temp directory for each test functions
    sets some environment variables frum needs

    View Slide

  58. Tips for good E2E testing
    If stdout or stderr is not as expected, the error is
    displayed like the following:

    View Slide

  59. Tips to create CLI with multiple
    subcommands

    View Slide

  60. View Slide

  61. ⬅︎
    function you need to implement

    View Slide

  62. View Slide

  63. --ruby-build-mirror
    ⬅︎

    View Slide

  64. ⬅︎
    Custom Error Type

    View Slide

  65. ⬅︎
    Custom Error Type
    ⬅︎install command arguments

    View Slide

  66. ⬅︎
    Custom Error Type
    ⬅︎install command arguments
    ⬅︎
    return custom error

    View Slide

  67. Tips to implement flexible
    Command-line completion using
    clap

    View Slide

  68. Tips to implement flexible
    Command-line completion using
    clap
    You can implement completion using
    clap::App::gen_completions
    However it is not useful enough

    View Slide

  69. That is because
    clap doesn't support adding additional code to complete
    values.

    View Slide

  70. That is because
    clap doesn't support adding additional code to complete
    values.

    View Slide

  71. In frum ⬅︎ prepare a empty vector

    View Slide

  72. In frum ⬅︎ prepare a empty vector
    store completions into buffer
    ⬅︎

    View Slide

  73. In frum ⬅︎ prepare a empty vector
    store completions into buffer
    ⬅︎
    ⬅︎
    iterating over lines in completions

    View Slide

  74. In frum

    View Slide

  75. In frum

    View Slide

  76. Conclusions

    View Slide

  77. Conclusions
    frum install runs a little bit faster than rbenv install
    frum init runs about 6x faster than rbenv init

    View Slide

  78. View Slide