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:
1 parent
107428010e
commit
bf7f923877
7 files changed
+130
-64
No files matched your search
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in new issue
Block a user