extension_host: Fix Windows manifest paths when uploading extensions to WSL remote (#50653)

Closes #42731 

Before you mark this PR as ready for review, make sure that you have:
- [X] Added a solid test coverage and/or screenshots from doing manual
testing
- [X] Done a self-review taking into account security and performance
aspects
- [X] Aligned any UI changes with the [UI
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)

<img width="1440" height="900" alt="Screenshot 2026-03-09 at 5 10 46 PM"
src="https://github.com/user-attachments/assets/bf124481-dc10-44e9-aaab-3e562d71e41e"
/>

Release Notes:

- Fixed Windows path handling in extension manifests to ensure
extensions upload correctly to remote environments like WSL.

---------

Co-authored-by: John Tur <john-tur@outlook.com>
This commit is contained in:
Thomas JensenandJohn Tur authored and GitHub committed 2026-04-21 17:32:54 -04:00
1 parent 107428010e
commit bf7f923877
7 files changed
+130 -64

No files matched your search

+10 -7
View File
@@ -15,7 +15,7 @@ use std::{
str::FromStr,
sync::Arc,
};
use util::command::Stdio;
use util::{command::Stdio, rel_path::PathExt};
use wasm_encoder::{ComponentSectionId, Encode as _, RawSection, Section as _};
use wasmparser::Parser;
@@ -108,7 +108,7 @@ impl ExtensionBuilder {
for (debug_adapter_name, meta) in &mut extension_manifest.debug_adapters {
let debug_adapter_schema_path =
extension_dir.join(build_debug_adapter_schema_path(debug_adapter_name, meta));
extension_dir.join(build_debug_adapter_schema_path(debug_adapter_name, meta)?);
let debug_adapter_schema = fs::read_to_string(&debug_adapter_schema_path)
.with_context(|| {
@@ -582,8 +582,9 @@ async fn populate_defaults(
let language_dir = language_dir?;
let config_path = language_dir.join(LanguageConfig::FILE_NAME);
if fs.is_file(config_path.as_path()).await {
let relative_language_dir =
language_dir.strip_prefix(extension_path)?.to_path_buf();
let relative_language_dir = language_dir
.strip_prefix(extension_path)?
.to_rel_path_buf()?;
if !manifest.languages.contains(&relative_language_dir) {
manifest.languages.push(relative_language_dir);
}
@@ -601,7 +602,8 @@ async fn populate_defaults(
while let Some(theme_path) = theme_dir_entries.next().await {
let theme_path = theme_path?;
if theme_path.extension() == Some("json".as_ref()) {
let relative_theme_path = theme_path.strip_prefix(extension_path)?.to_path_buf();
let relative_theme_path =
theme_path.strip_prefix(extension_path)?.to_rel_path_buf()?;
if !manifest.themes.contains(&relative_theme_path) {
manifest.themes.push(relative_theme_path);
}
@@ -619,8 +621,9 @@ async fn populate_defaults(
while let Some(icon_theme_path) = icon_theme_dir_entries.next().await {
let icon_theme_path = icon_theme_path?;
if icon_theme_path.extension() == Some("json".as_ref()) {
let relative_icon_theme_path =
icon_theme_path.strip_prefix(extension_path)?.to_path_buf();
let relative_icon_theme_path = icon_theme_path
.strip_prefix(extension_path)?
.to_rel_path_buf()?;
if !manifest.icon_themes.contains(&relative_icon_theme_path) {
manifest.icon_themes.push(relative_icon_theme_path);
}
+48 -31
View File
@@ -11,6 +11,7 @@ use language::LanguageName;
use lsp::LanguageServerName;
use semver::Version;
use serde::{Deserialize, Serialize};
use util::rel_path::{PathExt, RelPathBuf};
use crate::ExtensionCapability;
@@ -28,11 +29,11 @@ pub struct OldExtensionManifest {
pub authors: Vec<String>,
#[serde(default)]
pub themes: BTreeMap<Arc<str>, PathBuf>,
pub themes: BTreeMap<Arc<str>, RelPathBuf>,
#[serde(default)]
pub languages: BTreeMap<Arc<str>, PathBuf>,
pub languages: BTreeMap<Arc<str>, RelPathBuf>,
#[serde(default)]
pub grammars: BTreeMap<Arc<str>, PathBuf>,
pub grammars: BTreeMap<Arc<str>, RelPathBuf>,
}
/// The schema version of the [`ExtensionManifest`].
@@ -94,11 +95,11 @@ pub struct ExtensionManifest {
pub lib: LibManifestEntry,
#[serde(default)]
pub themes: Vec<PathBuf>,
pub themes: Vec<RelPathBuf>,
#[serde(default)]
pub icon_themes: Vec<PathBuf>,
pub icon_themes: Vec<RelPathBuf>,
#[serde(default)]
pub languages: Vec<PathBuf>,
pub languages: Vec<RelPathBuf>,
#[serde(default)]
pub grammars: BTreeMap<Arc<str>, GrammarManifestEntry>,
#[serde(default)]
@@ -195,11 +196,13 @@ impl ExtensionManifest {
pub fn build_debug_adapter_schema_path(
adapter_name: &Arc<str>,
meta: &DebugAdapterManifestEntry,
) -> PathBuf {
meta.schema_path.clone().unwrap_or_else(|| {
Path::new("debug_adapter_schemas")
) -> anyhow::Result<RelPathBuf> {
match &meta.schema_path {
Some(path) => Ok(path.clone()),
None => Path::new("debug_adapter_schemas")
.join(Path::new(adapter_name.as_ref()).with_extension("json"))
})
.to_rel_path_buf(),
}
}
#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)]
@@ -350,7 +353,7 @@ pub struct SlashCommandManifestEntry {
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
pub struct DebugAdapterManifestEntry {
pub schema_path: Option<PathBuf>,
pub schema_path: Option<RelPathBuf>,
}
#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)]
@@ -442,7 +445,9 @@ fn manifest_from_old_manifest(
#[cfg(test)]
mod tests {
use indoc::indoc;
use pretty_assertions::assert_eq;
use util::rel_path::rel_path_buf;
use crate::ProcessExecCapability;
@@ -478,11 +483,11 @@ mod tests {
fn test_build_adapter_schema_path_with_schema_path() {
let adapter_name = Arc::from("my_adapter");
let entry = DebugAdapterManifestEntry {
schema_path: Some(PathBuf::from("foo/bar")),
schema_path: Some(rel_path_buf("foo/bar")),
};
let path = build_debug_adapter_schema_path(&adapter_name, &entry);
assert_eq!(path, PathBuf::from("foo/bar"));
let path = build_debug_adapter_schema_path(&adapter_name, &entry).unwrap();
assert_eq!(path, rel_path_buf("foo/bar"));
}
#[test]
@@ -490,11 +495,8 @@ mod tests {
let adapter_name = Arc::from("my_adapter");
let entry = DebugAdapterManifestEntry { schema_path: None };
let path = build_debug_adapter_schema_path(&adapter_name, &entry);
assert_eq!(
path,
PathBuf::from("debug_adapter_schemas").join("my_adapter.json")
);
let path = build_debug_adapter_schema_path(&adapter_name, &entry).unwrap();
assert_eq!(path, rel_path_buf("debug_adapter_schemas/my_adapter.json"));
}
#[test]
@@ -572,22 +574,37 @@ mod tests {
);
assert!(manifest.allow_exec("docker", &["ps"]).is_err()); // wrong first arg
}
#[test]
#[cfg(target_os = "windows")]
fn test_deserialize_manifest_with_windows_separators() {
let content = indoc! {r#"
id = "test-manifest"
name = "Test Manifest"
version = "0.0.1"
schema_version = 0
languages = ["foo\\bar"]
"#};
let manifest: ExtensionManifest = toml::from_str(&content).expect("manifest should parse");
assert_eq!(manifest.languages, vec![rel_path_buf("foo/bar")]);
}
#[test]
fn parse_manifest_with_agent_server_archive_launcher() {
let toml_src = r#"
id = "example.agent-server-ext"
name = "Agent Server Example"
version = "1.0.0"
schema_version = 0
let toml_src = indoc! {r#"
id = "example.agent-server-ext"
name = "Agent Server Example"
version = "1.0.0"
schema_version = 0
[agent_servers.foo]
name = "Foo Agent"
[agent_servers.foo]
name = "Foo Agent"
[agent_servers.foo.targets.linux-x86_64]
archive = "https://example.com/agent-linux-x64.tar.gz"
cmd = "./agent"
args = ["--serve"]
"#;
[agent_servers.foo.targets.linux-x86_64]
archive = "https://example.com/agent-linux-x64.tar.gz"
cmd = "./agent"
args = ["--serve"]
"#};
let manifest: ExtensionManifest = toml::from_str(toml_src).expect("manifest should parse");
assert_eq!(manifest.id.as_ref(), "example.agent-server-ext");
+6 -6
View File
@@ -165,6 +165,7 @@ async fn copy_extension_resources(
let output_themes_dir = output_dir.join("themes");
fs::create_dir_all(&output_themes_dir)?;
for theme_path in &manifest.themes {
let theme_path = theme_path.as_std_path();
fs::copy(
extension_path.join(theme_path),
output_themes_dir.join(theme_path.file_name().context("invalid theme path")?),
@@ -177,6 +178,7 @@ async fn copy_extension_resources(
let output_icon_themes_dir = output_dir.join("icon_themes");
fs::create_dir_all(&output_icon_themes_dir)?;
for icon_theme_path in &manifest.icon_themes {
let icon_theme_path = icon_theme_path.as_std_path();
fs::copy(
extension_path.join(icon_theme_path),
output_icon_themes_dir.join(
@@ -224,6 +226,7 @@ async fn copy_extension_resources(
let output_languages_dir = output_dir.join("languages");
fs::create_dir_all(&output_languages_dir)?;
for language_path in &manifest.languages {
let language_path = language_path.as_std_path();
copy_recursive(
fs.as_ref(),
&extension_path.join(language_path),
@@ -243,14 +246,11 @@ async fn copy_extension_resources(
if !manifest.debug_adapters.is_empty() {
for (debug_adapter, entry) in &manifest.debug_adapters {
let schema_path = entry.schema_path.clone().unwrap_or_else(|| {
PathBuf::from("debug_adapter_schemas".to_owned())
.join(debug_adapter.as_ref())
.with_extension("json")
});
let schema_path = extension::build_debug_adapter_schema_path(debug_adapter, entry)?;
let parent = schema_path
.parent()
.with_context(|| format!("invalid empty schema path for {debug_adapter}"))?;
let schema_path = schema_path.as_std_path();
fs::create_dir_all(output_dir.join(parent))?;
copy_recursive(
fs.as_ref(),
@@ -265,7 +265,7 @@ async fn copy_extension_resources(
.with_context(|| {
format!(
"failed to copy debug adapter schema '{}'",
schema_path.display()
schema_path.display(),
)
})?;
}
+16 -13
View File
@@ -56,7 +56,7 @@ use std::{
};
use task::TaskTemplates;
use url::Url;
use util::{ResultExt, paths::RemotePathBuf};
use util::{ResultExt, paths::RemotePathBuf, rel_path::PathExt};
use wasm_host::{
WasmExtension, WasmHost,
wit::{is_supported_wasm_api_version, wasm_api_version_range},
@@ -1244,13 +1244,16 @@ impl ExtensionStore {
}));
themes_to_add.extend(extension.manifest.themes.iter().map(|theme_path| {
let mut path = self.installed_dir.clone();
path.extend([Path::new(extension_id.as_ref()), theme_path.as_path()]);
path.extend([Path::new(extension_id.as_ref()), theme_path.as_std_path()]);
path
}));
icon_themes_to_add.extend(extension.manifest.icon_themes.iter().map(
|icon_theme_path| {
let mut path = self.installed_dir.clone();
path.extend([Path::new(extension_id.as_ref()), icon_theme_path.as_path()]);
path.extend([
Path::new(extension_id.as_ref()),
icon_theme_path.as_std_path(),
]);
let mut icons_root_path = self.installed_dir.clone();
icons_root_path.extend([Path::new(extension_id.as_ref())]);
@@ -1560,7 +1563,7 @@ impl ExtensionStore {
})?;
let config = ::toml::from_str::<LanguageConfig>(&config)?;
let relative_path = relative_path.to_path_buf();
let relative_path = relative_path.to_rel_path_buf()?;
if !extension_manifest.languages.contains(&relative_path) {
extension_manifest.languages.push(relative_path.clone());
}
@@ -1569,7 +1572,7 @@ impl ExtensionStore {
config.name.clone(),
ExtensionIndexLanguageEntry {
extension: extension_id.clone(),
path: relative_path,
path: relative_path.as_std_path().to_path_buf(),
matcher: config.matcher,
hidden: config.hidden,
grammar: config.grammar,
@@ -1593,7 +1596,7 @@ impl ExtensionStore {
continue;
};
let relative_path = relative_path.to_path_buf();
let relative_path = relative_path.to_rel_path_buf()?;
if !extension_manifest.themes.contains(&relative_path) {
extension_manifest.themes.push(relative_path.clone());
}
@@ -1603,7 +1606,7 @@ impl ExtensionStore {
theme_name.into(),
ExtensionIndexThemeEntry {
extension: extension_id.clone(),
path: relative_path.clone(),
path: relative_path.as_std_path().to_path_buf(),
},
);
}
@@ -1625,7 +1628,7 @@ impl ExtensionStore {
continue;
};
let relative_path = relative_path.to_path_buf();
let relative_path = relative_path.to_rel_path_buf()?;
if !extension_manifest.icon_themes.contains(&relative_path) {
extension_manifest.icon_themes.push(relative_path.clone());
}
@@ -1635,7 +1638,7 @@ impl ExtensionStore {
icon_theme_name.into(),
ExtensionIndexIconThemeEntry {
extension: extension_id.clone(),
path: relative_path.clone(),
path: relative_path.as_std_path().to_path_buf(),
},
);
}
@@ -1721,15 +1724,15 @@ impl ExtensionStore {
}
for (adapter_name, meta) in loaded_extension.manifest.debug_adapters.iter() {
let schema_path = &extension::build_debug_adapter_schema_path(adapter_name, meta);
let schema_path = extension::build_debug_adapter_schema_path(adapter_name, meta)?;
if fs.is_file(&src_dir.join(schema_path)).await {
if fs.is_file(&src_dir.join(&schema_path)).await {
if let Some(parent) = schema_path.parent() {
fs.create_dir(&tmp_dir.join(parent)).await?
}
fs.copy_file(
&src_dir.join(schema_path),
&tmp_dir.join(schema_path),
&src_dir.join(&schema_path),
&tmp_dir.join(&schema_path),
fs::CopyOptions::default(),
)
.await?
@@ -26,7 +26,7 @@ use std::{
sync::Arc,
};
use theme::ThemeRegistry;
use util::test::TempTree;
use util::{rel_path::rel_path_buf, test::TempTree};
#[cfg(test)]
#[ctor::ctor]
@@ -150,7 +150,10 @@ async fn test_extension_store(cx: &mut TestAppContext) {
themes: Default::default(),
icon_themes: Vec::new(),
lib: Default::default(),
languages: vec!["languages/erb".into(), "languages/ruby".into()],
languages: vec![
rel_path_buf("languages/erb"),
rel_path_buf("languages/ruby"),
],
grammars: [
("embedded_template".into(), GrammarManifestEntry::default()),
("ruby".into(), GrammarManifestEntry::default()),
@@ -182,8 +185,8 @@ async fn test_extension_store(cx: &mut TestAppContext) {
authors: vec![],
repository: None,
themes: vec![
"themes/monokai-pro.json".into(),
"themes/monokai.json".into(),
rel_path_buf("themes/monokai-pro.json"),
rel_path_buf("themes/monokai.json"),
],
icon_themes: Vec::new(),
lib: Default::default(),
@@ -367,7 +370,7 @@ async fn test_extension_store(cx: &mut TestAppContext) {
description: None,
authors: vec![],
repository: None,
themes: vec!["themes/gruvbox.json".into()],
themes: vec![rel_path_buf("themes/gruvbox.json")],
icon_themes: Vec::new(),
lib: Default::default(),
languages: Default::default(),
+1 -1
View File
@@ -194,7 +194,7 @@ impl HeadlessExtensionStore {
}
for (debug_adapter, meta) in &manifest.debug_adapters {
let schema_path = extension::build_debug_adapter_schema_path(debug_adapter, meta);
let schema_path = extension::build_debug_adapter_schema_path(debug_adapter, meta)?;
this.update(cx, |this, _cx| {
this.proxy.register_debug_adapter(
+41 -1
View File
@@ -27,7 +27,7 @@ pub struct RelPath(str);
/// relative and normalized.
///
/// This type is to [`RelPath`] as [`std::path::PathBuf`] is to [`std::path::Path`]
#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
#[derive(PartialEq, Eq, Clone, Ord, PartialOrd, Serialize)]
pub struct RelPathBuf(String);
impl RelPath {
@@ -333,12 +333,36 @@ impl RelPathBuf {
}
}
impl<'de> Deserialize<'de> for RelPathBuf {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let path = String::deserialize(deserializer)?;
let rel_path =
RelPath::new(Path::new(&path), PathStyle::local()).map_err(serde::de::Error::custom)?;
Ok(rel_path.into_owned())
}
}
impl Into<Arc<RelPath>> for RelPathBuf {
fn into(self) -> Arc<RelPath> {
Arc::from(self.as_rel_path())
}
}
impl AsRef<Path> for RelPathBuf {
fn as_ref(&self) -> &Path {
self.as_std_path()
}
}
impl AsRef<Path> for RelPath {
fn as_ref(&self) -> &Path {
self.as_std_path()
}
}
impl AsRef<RelPath> for RelPathBuf {
fn as_ref(&self) -> &RelPath {
self.as_rel_path()
@@ -378,12 +402,28 @@ pub fn rel_path(path: &str) -> &RelPath {
RelPath::unix(path).unwrap()
}
#[cfg(any(test, feature = "test-support"))]
#[track_caller]
pub fn rel_path_buf(path: &str) -> RelPathBuf {
RelPath::unix(path).unwrap().to_rel_path_buf()
}
impl PartialEq<str> for RelPath {
fn eq(&self, other: &str) -> bool {
self.0 == *other
}
}
pub trait PathExt {
fn to_rel_path_buf(&self) -> Result<RelPathBuf>;
}
impl<T: AsRef<Path> + ?Sized> PathExt for T {
fn to_rel_path_buf(&self) -> Result<RelPathBuf> {
Ok(RelPath::new(self.as_ref(), PathStyle::local())?.into_owned())
}
}
#[derive(Default)]
pub struct RelPathComponents<'a>(&'a str);