Skip to content

Commit d11bc41

Browse files
committed
fix(ruby): retry Ruby tool resolution with a real cwd
The previous PWD-based attempt did not change the child process working directory, so Bundler and RubyGems could still resolve the wrong Ruby. Run extension-side bundle/ruby/gem probes through a worktree-root shell wrapper instead, and allow that wrapper in the manifest so fallback installation uses the same project context as the final language server.
1 parent 04a6a51 commit d11bc41

7 files changed

Lines changed: 105 additions & 68 deletions

File tree

extension.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,5 +89,10 @@ kind = "process:exec"
8989
command = "ruby"
9090
args = ["--version"]
9191

92+
[[capabilities]]
93+
kind = "process:exec"
94+
command = "sh"
95+
args = ["-c", "*"]
96+
9297
[debug_adapters.rdbg]
9398
[debug_locators.ruby]

src/bundler.rs

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{command_executor::CommandExecutor, environment::shell_env_with_pwd};
1+
use crate::command_executor::CommandExecutor;
22
use anyhow::{bail, Context, Result};
33
use std::path::PathBuf;
44

@@ -53,22 +53,27 @@ impl<E: CommandExecutor> Bundler<E> {
5353
})?;
5454

5555
let full_args: Vec<&str> = std::iter::once(cmd).chain(args.iter().copied()).collect();
56-
let mut command_envs: Vec<(String, String)> = envs
56+
let command_envs: Vec<(String, String)> = envs
5757
.iter()
5858
.copied()
5959
.filter(|(key, _)| *key != "BUNDLE_GEMFILE")
6060
.map(|(key, value)| (key.to_string(), value.to_string()))
6161
.collect();
62-
command_envs.push(("BUNDLE_GEMFILE".to_string(), bundle_gemfile.to_string()));
63-
let command_envs = shell_env_with_pwd(command_envs, working_dir_str);
62+
let command_envs = command_envs
63+
.into_iter()
64+
.chain(std::iter::once((
65+
"BUNDLE_GEMFILE".to_string(),
66+
bundle_gemfile.to_string(),
67+
)))
68+
.collect::<Vec<_>>();
6469
let command_env_refs = command_envs
6570
.iter()
6671
.map(|(key, value)| (key.as_str(), value.as_str()))
6772
.collect::<Vec<_>>();
6873

6974
let output = self
7075
.command_executor
71-
.execute("bundle", &full_args, &command_env_refs)
76+
.execute_in_dir("bundle", &full_args, &command_env_refs, working_dir_str)
7277
.map_err(|e| anyhow::anyhow!(e))?;
7378

7479
match output.status {
@@ -171,10 +176,7 @@ mod tests {
171176
.to_string_lossy()
172177
.into_owned();
173178

174-
vec![
175-
("BUNDLE_GEMFILE".to_string(), gemfile_path),
176-
("PWD".to_string(), dir.to_string()),
177-
]
179+
vec![("BUNDLE_GEMFILE".to_string(), gemfile_path)]
178180
}
179181

180182
fn env_refs(envs: &[(String, String)]) -> Vec<(&str, &str)> {
@@ -216,7 +218,7 @@ mod tests {
216218
}
217219

218220
#[test]
219-
fn test_installed_gem_version_overrides_pwd_and_bundle_gemfile_envs() {
221+
fn test_installed_gem_version_overrides_bundle_gemfile_env() {
220222
let mock_executor = MockCommandExecutor::new();
221223
let mut expected_command_envs = vec![("PATH".to_string(), "/usr/bin".to_string())];
222224
expected_command_envs.extend(expected_envs("test_dir"));
@@ -237,11 +239,7 @@ mod tests {
237239
let version = bundler
238240
.installed_gem_version(
239241
"rails",
240-
&[
241-
("PWD", "wrong_dir"),
242-
("BUNDLE_GEMFILE", "wrong/Gemfile"),
243-
("PATH", "/usr/bin"),
244-
],
242+
&[("BUNDLE_GEMFILE", "wrong/Gemfile"), ("PATH", "/usr/bin")],
245243
)
246244
.expect("Expected successful version");
247245

src/command_executor.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,17 @@ pub trait CommandExecutor {
2222
args: &[&str],
2323
envs: &[(&str, &str)],
2424
) -> zed::Result<zed::process::Output>;
25+
26+
fn execute_in_dir(
27+
&self,
28+
cmd: &str,
29+
args: &[&str],
30+
envs: &[(&str, &str)],
31+
working_dir: &str,
32+
) -> zed::Result<zed::process::Output> {
33+
let _ = working_dir;
34+
self.execute(cmd, args, envs)
35+
}
2536
}
2637

2738
/// An implementation of `CommandExecutor` that executes commands
@@ -41,4 +52,36 @@ impl CommandExecutor for RealCommandExecutor {
4152
.envs(envs.iter().copied())
4253
.output()
4354
}
55+
56+
fn execute_in_dir(
57+
&self,
58+
cmd: &str,
59+
args: &[&str],
60+
envs: &[(&str, &str)],
61+
working_dir: &str,
62+
) -> zed::Result<zed::process::Output> {
63+
let script = sh_command_in_dir(working_dir, cmd, args);
64+
65+
eprintln!("Executing in dir via sh: {script}");
66+
67+
zed::Command::new("sh")
68+
.args(["-c", script.as_str()])
69+
.envs(envs.iter().copied())
70+
.output()
71+
}
72+
}
73+
74+
fn sh_command_in_dir(working_dir: &str, cmd: &str, args: &[&str]) -> String {
75+
format!(
76+
"cd -- {} && exec {}{}",
77+
sh_quote(working_dir),
78+
sh_quote(cmd),
79+
args.iter()
80+
.map(|arg| format!(" {}", sh_quote(arg)))
81+
.collect::<String>()
82+
)
83+
}
84+
85+
fn sh_quote(value: &str) -> String {
86+
format!("'{}'", value.replace('\'', "'\"'\"'"))
4487
}

src/environment.rs

Lines changed: 0 additions & 40 deletions
This file was deleted.

src/gemset.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,17 @@ pub fn versioned_gem_home(
1313
envs: &[(&str, &str)],
1414
executor: &dyn CommandExecutor,
1515
) -> Result<PathBuf> {
16+
let working_dir = envs
17+
.iter()
18+
.find_map(|(key, value)| (*key == "PWD").then_some(*value));
19+
1620
let output = executor
17-
.execute("ruby", &["--version"], envs)
21+
.execute_in_dir(
22+
"ruby",
23+
&["--version"],
24+
envs,
25+
working_dir.unwrap_or(base_dir.to_string_lossy().as_ref()),
26+
)
1827
.map_err(|e| anyhow::anyhow!(e))
1928
.context("Failed to detect Ruby version")?;
2029

@@ -34,6 +43,7 @@ pub fn versioned_gem_home(
3443
/// A simple wrapper around the `gem` command.
3544
pub struct Gemset {
3645
gem_home: PathBuf,
46+
working_dir: Option<String>,
3747
envs: Vec<(String, String)>,
3848
cached_env: OnceLock<Vec<(String, String)>>,
3949
command_executor: Box<dyn CommandExecutor>,
@@ -47,6 +57,10 @@ impl Gemset {
4757
) -> Self {
4858
Self {
4959
gem_home,
60+
working_dir: envs.and_then(|envs| {
61+
envs.iter()
62+
.find_map(|(key, value)| (*key == "PWD").then_some((*value).to_string()))
63+
}),
5064
envs: envs.map_or(Vec::new(), |envs| {
5165
envs.iter()
5266
.map(|&(k, v)| (k.to_string(), v.to_string()))
@@ -181,7 +195,14 @@ impl Gemset {
181195

182196
let output = self
183197
.command_executor
184-
.execute("gem", &full_args, &merged_envs)
198+
.execute_in_dir(
199+
"gem",
200+
&full_args,
201+
&merged_envs,
202+
self.working_dir
203+
.as_deref()
204+
.unwrap_or(self.gem_home.to_string_lossy().as_ref()),
205+
)
185206
.map_err(|e| anyhow!(e))?;
186207

187208
match output.status {

src/language_servers/language_server.rs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ use std::collections::HashMap;
44
use crate::{
55
bundler::Bundler,
66
command_executor::RealCommandExecutor,
7-
environment::shell_env_with_pwd,
87
gemset::{versioned_gem_home, Gemset},
98
};
109
use std::path::PathBuf;
@@ -151,7 +150,7 @@ pub trait LanguageServer {
151150
zed::settings::LspSettings::for_worktree(language_server_id.as_ref(), worktree)?;
152151

153152
let worktree_root = worktree.root_path();
154-
let shell_env = shell_env_with_pwd(worktree.shell_env(), worktree_root.clone());
153+
let shell_env = worktree.shell_env();
155154

156155
if let Some(binary_settings) = &lsp_settings.binary {
157156
if let Some(path) = &binary_settings.path {
@@ -197,7 +196,14 @@ pub trait LanguageServer {
197196
env: Some(shell_env),
198197
})
199198
}
200-
Err(_e) => self.try_find_on_path_or_extension_gemset(language_server_id, worktree),
199+
Err(e) => {
200+
eprintln!(
201+
"Bundler probe failed for {} in {}: {e:#}",
202+
Self::GEM_NAME,
203+
worktree_root
204+
);
205+
self.try_find_on_path_or_extension_gemset(language_server_id, worktree)
206+
}
201207
}
202208
}
203209

@@ -206,8 +212,7 @@ pub trait LanguageServer {
206212
language_server_id: &zed::LanguageServerId,
207213
worktree: &zed::Worktree,
208214
) -> zed::Result<LanguageServerBinary> {
209-
let worktree_root = worktree.root_path();
210-
let shell_env = shell_env_with_pwd(worktree.shell_env(), worktree_root);
215+
let shell_env = worktree.shell_env();
211216

212217
if let Some(path) = worktree.which(Self::EXECUTABLE_NAME) {
213218
Ok(LanguageServerBinary {
@@ -228,8 +233,7 @@ pub trait LanguageServer {
228233
let base_dir = std::env::current_dir()
229234
.map_err(|e| format!("Failed to get extension directory: {e:#}"))?;
230235

231-
let worktree_root = worktree.root_path();
232-
let worktree_shell_env = shell_env_with_pwd(worktree.shell_env(), worktree_root);
236+
let worktree_shell_env = worktree.shell_env();
233237
let worktree_shell_env_vars: Vec<(&str, &str)> = worktree_shell_env
234238
.iter()
235239
.map(|(key, value)| (key.as_str(), value.as_str()))
@@ -239,6 +243,12 @@ pub trait LanguageServer {
239243
versioned_gem_home(&base_dir, &worktree_shell_env_vars, &RealCommandExecutor)
240244
.map_err(|e| format!("{:#}", e))?;
241245

246+
eprintln!(
247+
"Using extension gemset for {} with gem home {}",
248+
Self::GEM_NAME,
249+
gem_home.display()
250+
);
251+
242252
let gemset = Gemset::new(
243253
gem_home,
244254
Some(&worktree_shell_env_vars),

src/ruby.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
mod bundler;
22
mod command_executor;
3-
mod environment;
43
mod gemset;
54
mod language_servers;
65

@@ -135,14 +134,15 @@ impl zed::Extension for RubyExtension {
135134
_: Option<String>,
136135
worktree: &Worktree,
137136
) -> Result<DebugAdapterBinary, String> {
137+
let worktree_root = worktree.root_path();
138138
let shell_env = worktree.shell_env();
139139
let env_vars: Vec<(&str, &str)> = shell_env
140140
.iter()
141141
.map(|(key, value)| (key.as_str(), value.as_str()))
142142
.collect();
143143

144144
let (command, mut arguments) = {
145-
let bundler = Bundler::new(PathBuf::from(worktree.root_path()), RealCommandExecutor);
145+
let bundler = Bundler::new(PathBuf::from(&worktree_root), RealCommandExecutor);
146146
if bundler.installed_gem_version("debug", &env_vars).is_ok() {
147147
let bundle = worktree.which("bundle").ok_or_else(|| {
148148
"debug gem present, but unable to find 'bundle' command".to_string()
@@ -177,7 +177,7 @@ impl zed::Extension for RubyExtension {
177177
if let Some(configuration) = configuration.as_object_mut() {
178178
configuration
179179
.entry("cwd")
180-
.or_insert_with(|| worktree.root_path().into());
180+
.or_insert_with(|| worktree_root.clone().into());
181181
}
182182

183183
let ruby_config: RubyDebugConfig = serde_json::from_value(configuration.clone())
@@ -240,7 +240,7 @@ impl zed::Extension for RubyExtension {
240240
command: Some(command),
241241
arguments,
242242
connection: Some(connection),
243-
cwd: ruby_config.cwd.or(Some(worktree.root_path())),
243+
cwd: ruby_config.cwd.or(Some(worktree_root)),
244244
envs: ruby_config.env.into_iter().collect(),
245245
request_args: StartDebuggingRequestArguments {
246246
configuration: configuration.to_string(),

0 commit comments

Comments
 (0)