Add release-grade logic for finding legacy Vagrant

After lots of experimentation I have landed on this as my proposal for
how we have our Go binary find its Ruby counterpart: just have it grab
it from the $PATH! @evanphx showed me this neat trick where by borrowing
a couple of helper methods from `exec` and tweaking them we can get
logic that will do a $PATH lookup that excludes "ourself". This allows
us to have both `vagrant` executables on the path... and means that
switching between Gogo-by-default or Legacy-by-default is just a matter
of tweaking $PATH order.

It _also_ means that we don't need any different lookup logic for
"release mode" vs "development mode" which is what I was looking at
before this solution.

In order to continue to facilitate development, I've generated a binstub
for vagrant using `bundle binstubs vagrant --standalone --path
./binstubs`, and I've updated the Nix development setup to prepend this
directory to the $PATH.

NOTE: Non-Nix users will need to modify their $PATH in the same way to
get the same behavior in development.
This commit is contained in:
Paul Hinze 2022-03-30 14:31:20 -05:00
parent 73a1be95fe
commit 4c21cb6ae5
No known key found for this signature in database
GPG Key ID: B69DEDF2D55501C0
3 changed files with 81 additions and 16 deletions

14
binstubs/vagrant Executable file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env ruby
#
# This file was generated by Bundler.
#
# The application 'vagrant' is installed as part of a gem, and
# this file is here to facilitate running it.
#
require "pathname"
path = Pathname.new(__FILE__)
$:.unshift File.expand_path "../..", path.realpath
require "bundler/setup"
load File.expand_path "../../bin/vagrant", path.realpath

View File

@ -2,11 +2,12 @@ package client
import (
"context"
"io/fs"
"net"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/golang/protobuf/ptypes/empty"
@ -180,22 +181,20 @@ func (c *Client) initLocalServer(ctx context.Context) (_ *grpc.ClientConn, err e
return client.Conn(), nil
}
// initVagrantRubyRuntime launches legacy vagrant as a gRPC server using the
// "serve" command.
//
// NOTE: We are assuming that the first executable we find in $PATH that is not
// _us_ is the legacy vagrant executable. It's up the packaging to ensure
// that is how things are set up.
func (c *Client) initVagrantRubyRuntime() (rubyRuntime plugin.ClientProtocol, err error) {
// TODO: Update for actual release usage. This is dev only now.
_, this_dir, _, _ := runtime.Caller(0)
cmd := exec.Command(
"bundle", "exec", "vagrant", "serve",
)
level := os.Getenv("VAGRANT_LOG")
cmd.Env = []string{
"BUNDLE_GEMFILE=" + filepath.Join(this_dir, "../../..", "Gemfile"),
"VAGRANT_I_KNOW_WHAT_IM_DOING_PLEASE_BE_QUIET=true",
"VAGRANT_LOG=" + level,
"VAGRANT_LOG_FILE=/tmp/vagrant.log",
var vagrantPath string
vagrantPath, err = lookPathSkippingSelf("vagrant")
if err != nil {
return
}
config := serverclient.RubyVagrantPluginConfig(c.logger)
config.Cmd = cmd
config.Cmd = exec.Command(vagrantPath, "serve")
rc := plugin.NewClient(config)
if _, err = rc.Start(); err != nil {
return
@ -235,3 +234,51 @@ func (c *Client) negotiateApiVersion(ctx context.Context) error {
c.logger.Info("negotiated api version", "version", vsn)
return nil
}
// lookPathSkippingSelf is a copy of exec.LookPath modified to skip the
// currently running executable.
func lookPathSkippingSelf(file string) (string, error) {
myselfPath, err := os.Executable()
if err != nil {
return "", err
}
myself, err := os.Stat(myselfPath)
if err != nil {
return "", err
}
if strings.Contains(file, "/") {
err := findExecutable(file, myself)
if err == nil {
return file, nil
}
return "", &exec.Error{Name: file, Err: err}
}
path := os.Getenv("PATH")
for _, dir := range filepath.SplitList(path) {
if dir == "" {
// Unix shell semantics: path element "" means "."
dir = "."
}
path := filepath.Join(dir, file)
if err := findExecutable(path, myself); err == nil {
return path, nil
}
}
return "", &exec.Error{Name: file, Err: exec.ErrNotFound}
}
// findExecutableSkippingSelf is a copy of exec.findExecutable modified to skip
// the provided FileInfo. It's used to power lookPathSkippingSelf.
func findExecutable(file string, skip os.FileInfo) error {
d, err := os.Stat(file)
if err != nil {
return err
}
if os.SameFile(d, skip) {
return fs.ErrPermission
}
if m := d.Mode(); !m.IsDir() && m&0111 != 0 {
return nil
}
return fs.ErrPermission
}

View File

@ -61,9 +61,13 @@ mkShell rec {
zlib
];
# workaround for npm/gulp dep compilation
# https://github.com/imagemin/optipng-bin/issues/108
shellHook = ''
# workaround for npm/gulp dep compilation
# https://github.com/imagemin/optipng-bin/issues/108
LD=$CC
# Prepend binstubs to PATH for development, which causes Vagrant-agogo
# to use the legacy Vagrant in this repo. See client.initVagrantRubyRuntime
PATH=$PWD/binstubs:$PATH
'';
}