1use std::collections::{BTreeSet, HashMap};
2use std::env;
3use std::ffi::OsString;
4use std::fs;
5use std::path::{Path, PathBuf};
6use std::process::Command;
7use std::sync::Arc;
8
9use ::fs::{CopyOptions, Fs, RealFs, copy_recursive};
10use anyhow::{Context as _, Result, anyhow, bail};
11use clap::Parser;
12use extension::ExtensionManifest;
13use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
14use language::LanguageConfig;
15use reqwest_client::ReqwestClient;
16use rpc::ExtensionProvides;
17use tree_sitter::{Language, Query, WasmStore};
18
19pub struct WrappedExtensionBuilder {}
20
21impl WrappedExtensionBuilder {
22 pub fn new(build_dir: PathBuf) -> Self {
23 Self {}
24 }
25}
26
27#[derive(Parser, Debug)]
28#[command(name = "zed-extension")]
29struct Args {
30 /// The path to the extension directory
31 #[arg(long)]
32 source_dir: PathBuf,
33 /// The output directory to place the packaged extension.
34 #[arg(long)]
35 output_dir: PathBuf,
36 /// The path to a directory where build dependencies are downloaded
37 #[arg(long)]
38 scratch_dir: PathBuf,
39}
40
41pub async fn run(extension_path: PathBuf, scratch_dir: PathBuf) -> Result<()> {
42 env_logger::init();
43
44 let fs = Arc::new(RealFs::new(None, gpui::background_executor()));
45 let engine = wasmtime::Engine::default();
46 let mut wasm_store = WasmStore::new(&engine)?;
47
48 log::info!("loading extension manifest");
49 let mut manifest = ExtensionManifest::load(fs.clone(), &extension_path).await?;
50
51 log::info!("compiling extension");
52
53 let user_agent = format!(
54 "Zed Extension CLI/{} ({}; {})",
55 env!("CARGO_PKG_VERSION"),
56 std::env::consts::OS,
57 std::env::consts::ARCH
58 );
59 let http_client = Arc::new(ReqwestClient::user_agent(&user_agent)?);
60
61 let builder = ExtensionBuilder::new(http_client, scratch_dir);
62 builder
63 .compile_extension(
64 &extension_path,
65 &mut manifest,
66 CompileExtensionOptions { release: true },
67 )
68 .await
69 .context("failed to compile extension")?;
70
71 let grammars = test_grammars(&manifest, &extension_path, &mut wasm_store)?;
72 test_languages(&manifest, &extension_path, &grammars)?;
73 test_themes(&manifest, &extension_path, fs.clone()).await?;
74
75 Ok(())
76}
77
78/// Returns the set of features provided by the extension.
79fn extension_provides(manifest: &ExtensionManifest) -> BTreeSet<ExtensionProvides> {
80 let mut provides = BTreeSet::default();
81 if !manifest.themes.is_empty() {
82 provides.insert(ExtensionProvides::Themes);
83 }
84
85 if !manifest.icon_themes.is_empty() {
86 provides.insert(ExtensionProvides::IconThemes);
87 }
88
89 if !manifest.languages.is_empty() {
90 provides.insert(ExtensionProvides::Languages);
91 }
92
93 if !manifest.grammars.is_empty() {
94 provides.insert(ExtensionProvides::Grammars);
95 }
96
97 if !manifest.language_servers.is_empty() {
98 provides.insert(ExtensionProvides::LanguageServers);
99 }
100
101 if !manifest.context_servers.is_empty() {
102 provides.insert(ExtensionProvides::ContextServers);
103 }
104
105 if manifest.snippets.is_some() {
106 provides.insert(ExtensionProvides::Snippets);
107 }
108
109 if !manifest.debug_adapters.is_empty() {
110 provides.insert(ExtensionProvides::DebugAdapters);
111 }
112
113 provides
114}
115
116async fn copy_extension_resources(
117 manifest: &ExtensionManifest,
118 extension_path: &Path,
119 output_dir: &Path,
120 fs: Arc<dyn Fs>,
121) -> Result<()> {
122 fs::create_dir_all(output_dir).context("failed to create output dir")?;
123
124 let manifest_toml = toml::to_string(&manifest).context("failed to serialize manifest")?;
125 fs::write(output_dir.join("extension.toml"), &manifest_toml)
126 .context("failed to write extension.toml")?;
127
128 if manifest.lib.kind.is_some() {
129 fs::copy(
130 extension_path.join("extension.wasm"),
131 output_dir.join("extension.wasm"),
132 )
133 .context("failed to copy extension.wasm")?;
134 }
135
136 if !manifest.grammars.is_empty() {
137 let source_grammars_dir = extension_path.join("grammars");
138 let output_grammars_dir = output_dir.join("grammars");
139 fs::create_dir_all(&output_grammars_dir)?;
140 for grammar_name in manifest.grammars.keys() {
141 let mut grammar_filename = PathBuf::from(grammar_name.as_ref());
142 grammar_filename.set_extension("wasm");
143 fs::copy(
144 source_grammars_dir.join(&grammar_filename),
145 output_grammars_dir.join(&grammar_filename),
146 )
147 .with_context(|| format!("failed to copy grammar '{}'", grammar_filename.display()))?;
148 }
149 }
150
151 if !manifest.themes.is_empty() {
152 let output_themes_dir = output_dir.join("themes");
153 fs::create_dir_all(&output_themes_dir)?;
154 for theme_path in &manifest.themes {
155 fs::copy(
156 extension_path.join(theme_path),
157 output_themes_dir.join(theme_path.file_name().context("invalid theme path")?),
158 )
159 .with_context(|| format!("failed to copy theme '{}'", theme_path.display()))?;
160 }
161 }
162
163 if !manifest.icon_themes.is_empty() {
164 let output_icon_themes_dir = output_dir.join("icon_themes");
165 fs::create_dir_all(&output_icon_themes_dir)?;
166 for icon_theme_path in &manifest.icon_themes {
167 fs::copy(
168 extension_path.join(icon_theme_path),
169 output_icon_themes_dir.join(
170 icon_theme_path
171 .file_name()
172 .context("invalid icon theme path")?,
173 ),
174 )
175 .with_context(|| {
176 format!("failed to copy icon theme '{}'", icon_theme_path.display())
177 })?;
178 }
179
180 let output_icons_dir = output_dir.join("icons");
181 fs::create_dir_all(&output_icons_dir)?;
182 copy_recursive(
183 fs.as_ref(),
184 &extension_path.join("icons"),
185 &output_icons_dir,
186 CopyOptions {
187 overwrite: true,
188 ignore_if_exists: false,
189 },
190 )
191 .await
192 .with_context(|| "failed to copy icons")?;
193 }
194
195 if !manifest.languages.is_empty() {
196 let output_languages_dir = output_dir.join("languages");
197 fs::create_dir_all(&output_languages_dir)?;
198 for language_path in &manifest.languages {
199 copy_recursive(
200 fs.as_ref(),
201 &extension_path.join(language_path),
202 &output_languages_dir
203 .join(language_path.file_name().context("invalid language path")?),
204 CopyOptions {
205 overwrite: true,
206 ignore_if_exists: false,
207 },
208 )
209 .await
210 .with_context(|| {
211 format!("failed to copy language dir '{}'", language_path.display())
212 })?;
213 }
214 }
215
216 if !manifest.debug_adapters.is_empty() {
217 for (debug_adapter, entry) in &manifest.debug_adapters {
218 let schema_path = entry.schema_path.clone().unwrap_or_else(|| {
219 PathBuf::from("debug_adapter_schemas".to_owned())
220 .join(debug_adapter.as_ref())
221 .with_extension("json")
222 });
223 let parent = schema_path
224 .parent()
225 .with_context(|| format!("invalid empty schema path for {debug_adapter}"))?;
226 fs::create_dir_all(output_dir.join(parent))?;
227 copy_recursive(
228 fs.as_ref(),
229 &extension_path.join(&schema_path),
230 &output_dir.join(&schema_path),
231 CopyOptions {
232 overwrite: true,
233 ignore_if_exists: false,
234 },
235 )
236 .await
237 .with_context(|| {
238 format!(
239 "failed to copy debug adapter schema '{}'",
240 schema_path.display()
241 )
242 })?;
243 }
244 }
245
246 if let Some(snippets_path) = manifest.snippets.as_ref() {
247 let parent = snippets_path.parent();
248 if let Some(parent) = parent.filter(|p| p.components().next().is_some()) {
249 fs::create_dir_all(output_dir.join(parent))?;
250 }
251 copy_recursive(
252 fs.as_ref(),
253 &extension_path.join(&snippets_path),
254 &output_dir.join(&snippets_path),
255 CopyOptions {
256 overwrite: true,
257 ignore_if_exists: false,
258 },
259 )
260 .await
261 .with_context(|| format!("failed to copy snippets from '{}'", snippets_path.display()))?;
262 }
263
264 Ok(())
265}
266
267fn test_grammars(
268 manifest: &ExtensionManifest,
269 extension_path: &Path,
270 wasm_store: &mut WasmStore,
271) -> Result<HashMap<String, Language>> {
272 let mut grammars = HashMap::default();
273 let grammars_dir = extension_path.join("grammars");
274
275 for grammar_name in manifest.grammars.keys() {
276 let mut grammar_path = grammars_dir.join(grammar_name.as_ref());
277 grammar_path.set_extension("wasm");
278
279 let wasm = fs::read(&grammar_path)?;
280 let language = wasm_store.load_language(grammar_name, &wasm)?;
281 log::info!("loaded grammar {grammar_name}");
282 grammars.insert(grammar_name.to_string(), language);
283 }
284
285 Ok(grammars)
286}
287
288fn test_languages(
289 manifest: &ExtensionManifest,
290 extension_path: &Path,
291 grammars: &HashMap<String, Language>,
292) -> Result<()> {
293 for relative_language_dir in &manifest.languages {
294 let language_dir = extension_path.join(relative_language_dir);
295 let config_path = language_dir.join("config.toml");
296 let config_content = fs::read_to_string(&config_path)?;
297 let config: LanguageConfig = toml::from_str(&config_content)?;
298 let grammar = if let Some(name) = &config.grammar {
299 Some(
300 grammars
301 .get(name.as_ref())
302 .with_context(|| format!("grammar not found: '{name}'"))?,
303 )
304 } else {
305 None
306 };
307
308 let query_entries = fs::read_dir(&language_dir)?;
309 for entry in query_entries {
310 let entry = entry?;
311 let query_path = entry.path();
312 if query_path.extension() == Some("scm".as_ref()) {
313 let grammar = grammar.with_context(|| {
314 format! {
315 "language {} provides query {} but no grammar",
316 config.name,
317 query_path.display()
318 }
319 })?;
320
321 let query_source = fs::read_to_string(&query_path)?;
322 let _query = Query::new(grammar, &query_source)
323 .map_err(|err| anyhow!("{}: {}", query_path.display(), err))?;
324 }
325 }
326
327 log::info!("loaded language {}", config.name);
328 }
329
330 Ok(())
331}
332
333async fn test_themes(
334 manifest: &ExtensionManifest,
335 extension_path: &Path,
336 fs: Arc<dyn Fs>,
337) -> Result<()> {
338 for relative_theme_path in &manifest.themes {
339 let theme_path = extension_path.join(relative_theme_path);
340 let theme_family = theme::read_user_theme(&theme_path, fs.clone()).await?;
341 log::info!("loaded theme family {}", theme_family.name);
342
343 for theme in &theme_family.themes {
344 if theme
345 .style
346 .colors
347 .deprecated_scrollbar_thumb_background
348 .is_some()
349 {
350 bail!(
351 r#"Theme "{theme_name}" is using a deprecated style property: scrollbar_thumb.background. Use `scrollbar.thumb.background` instead."#,
352 theme_name = theme.name
353 )
354 }
355 }
356 }
357
358 Ok(())
359}