1use std::{
2 collections::HashMap,
3 env, fs,
4 path::{Path, PathBuf},
5 process::Command,
6 sync::Arc,
7};
8
9use ::fs::{copy_recursive, CopyOptions, Fs, RealFs};
10use anyhow::{anyhow, bail, Context, Result};
11use clap::Parser;
12use extension::{
13 extension_builder::{CompileExtensionOptions, ExtensionBuilder},
14 ExtensionManifest,
15};
16use language::LanguageConfig;
17use theme::ThemeRegistry;
18use tree_sitter::{Language, Query, WasmStore};
19
20#[derive(Parser, Debug)]
21#[command(name = "zed-extension")]
22struct Args {
23 /// The path to the extension directory
24 #[arg(long)]
25 source_dir: PathBuf,
26 /// The output directory to place the packaged extension.
27 #[arg(long)]
28 output_dir: PathBuf,
29 /// The path to a directory where build dependencies are downloaded
30 #[arg(long)]
31 scratch_dir: PathBuf,
32}
33
34#[tokio::main]
35async fn main() -> Result<()> {
36 env_logger::init();
37
38 let args = Args::parse();
39 let fs = Arc::new(RealFs::default());
40 let engine = wasmtime::Engine::default();
41 let mut wasm_store = WasmStore::new(engine)?;
42
43 let extension_path = args
44 .source_dir
45 .canonicalize()
46 .context("failed to canonicalize source_dir")?;
47 let scratch_dir = args
48 .scratch_dir
49 .canonicalize()
50 .context("failed to canonicalize scratch_dir")?;
51 let output_dir = if args.output_dir.is_relative() {
52 env::current_dir()?.join(&args.output_dir)
53 } else {
54 args.output_dir
55 };
56
57 log::info!("loading extension manifest");
58 let mut manifest = ExtensionManifest::load(fs.clone(), &extension_path).await?;
59
60 log::info!("compiling extension");
61 let builder = ExtensionBuilder::new(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 let archive_dir = output_dir.join("archive");
76 fs::remove_dir_all(&archive_dir).ok();
77 copy_extension_resources(&manifest, &extension_path, &archive_dir, fs.clone())
78 .await
79 .context("failed to copy extension resources")?;
80
81 let tar_output = Command::new("tar")
82 .current_dir(&output_dir)
83 .args(&["-czvf", "archive.tar.gz", "-C", "archive", "."])
84 .output()
85 .context("failed to run tar")?;
86 if !tar_output.status.success() {
87 bail!(
88 "failed to create archive.tar.gz: {}",
89 String::from_utf8_lossy(&tar_output.stderr)
90 );
91 }
92
93 let manifest_json = serde_json::to_string(&rpc::ExtensionApiManifest {
94 name: manifest.name,
95 version: manifest.version,
96 description: manifest.description,
97 authors: manifest.authors,
98 schema_version: Some(manifest.schema_version.0),
99 repository: manifest
100 .repository
101 .ok_or_else(|| anyhow!("missing repository in extension manifest"))?,
102 wasm_api_version: manifest.lib.version.map(|version| version.to_string()),
103 })?;
104 fs::remove_dir_all(&archive_dir)?;
105 fs::write(output_dir.join("manifest.json"), manifest_json.as_bytes())?;
106
107 Ok(())
108}
109
110async fn copy_extension_resources(
111 manifest: &ExtensionManifest,
112 extension_path: &Path,
113 output_dir: &Path,
114 fs: Arc<dyn Fs>,
115) -> Result<()> {
116 fs::create_dir_all(&output_dir).context("failed to create output dir")?;
117
118 let manifest_toml = toml::to_string(&manifest).context("failed to serialize manifest")?;
119 fs::write(output_dir.join("extension.toml"), &manifest_toml)
120 .context("failed to write extension.toml")?;
121
122 if manifest.lib.kind.is_some() {
123 fs::copy(
124 extension_path.join("extension.wasm"),
125 output_dir.join("extension.wasm"),
126 )
127 .context("failed to copy extension.wasm")?;
128 }
129
130 if !manifest.grammars.is_empty() {
131 let source_grammars_dir = extension_path.join("grammars");
132 let output_grammars_dir = output_dir.join("grammars");
133 fs::create_dir_all(&output_grammars_dir)?;
134 for grammar_name in manifest.grammars.keys() {
135 let mut grammar_filename = PathBuf::from(grammar_name.as_ref());
136 grammar_filename.set_extension("wasm");
137 fs::copy(
138 &source_grammars_dir.join(&grammar_filename),
139 &output_grammars_dir.join(&grammar_filename),
140 )
141 .with_context(|| format!("failed to copy grammar '{}'", grammar_filename.display()))?;
142 }
143 }
144
145 if !manifest.themes.is_empty() {
146 let output_themes_dir = output_dir.join("themes");
147 fs::create_dir_all(&output_themes_dir)?;
148 for theme_path in &manifest.themes {
149 fs::copy(
150 extension_path.join(theme_path),
151 output_themes_dir.join(
152 theme_path
153 .file_name()
154 .ok_or_else(|| anyhow!("invalid theme path"))?,
155 ),
156 )
157 .with_context(|| format!("failed to copy theme '{}'", theme_path.display()))?;
158 }
159 }
160
161 if !manifest.languages.is_empty() {
162 let output_languages_dir = output_dir.join("languages");
163 fs::create_dir_all(&output_languages_dir)?;
164 for language_path in &manifest.languages {
165 copy_recursive(
166 fs.as_ref(),
167 &extension_path.join(language_path),
168 &output_languages_dir.join(
169 language_path
170 .file_name()
171 .ok_or_else(|| anyhow!("invalid language path"))?,
172 ),
173 CopyOptions {
174 overwrite: true,
175 ignore_if_exists: false,
176 },
177 )
178 .await
179 .with_context(|| {
180 format!("failed to copy language dir '{}'", language_path.display())
181 })?;
182 }
183 }
184
185 Ok(())
186}
187
188fn test_grammars(
189 manifest: &ExtensionManifest,
190 extension_path: &Path,
191 wasm_store: &mut WasmStore,
192) -> Result<HashMap<String, Language>> {
193 let mut grammars = HashMap::default();
194 let grammars_dir = extension_path.join("grammars");
195
196 for grammar_name in manifest.grammars.keys() {
197 let mut grammar_path = grammars_dir.join(grammar_name.as_ref());
198 grammar_path.set_extension("wasm");
199
200 let wasm = fs::read(&grammar_path)?;
201 let language = wasm_store.load_language(grammar_name, &wasm)?;
202 log::info!("loaded grammar {grammar_name}");
203 grammars.insert(grammar_name.to_string(), language);
204 }
205
206 Ok(grammars)
207}
208
209fn test_languages(
210 manifest: &ExtensionManifest,
211 extension_path: &Path,
212 grammars: &HashMap<String, Language>,
213) -> Result<()> {
214 for relative_language_dir in &manifest.languages {
215 let language_dir = extension_path.join(relative_language_dir);
216 let config_path = language_dir.join("config.toml");
217 let config_content = fs::read_to_string(&config_path)?;
218 let config: LanguageConfig = toml::from_str(&config_content)?;
219 let grammar = if let Some(name) = &config.grammar {
220 Some(
221 grammars
222 .get(name.as_ref())
223 .ok_or_else(|| anyhow!("grammar not found: '{name}'"))?,
224 )
225 } else {
226 None
227 };
228
229 let query_entries = fs::read_dir(&language_dir)?;
230 for entry in query_entries {
231 let entry = entry?;
232 let query_path = entry.path();
233 if query_path.extension() == Some("scm".as_ref()) {
234 let grammar = grammar.ok_or_else(|| {
235 anyhow!(
236 "language {} provides query {} but no grammar",
237 config.name,
238 query_path.display()
239 )
240 })?;
241
242 let query_source = fs::read_to_string(&query_path)?;
243 let _query = Query::new(grammar, &query_source)?;
244 }
245 }
246
247 log::info!("loaded language {}", config.name);
248 }
249
250 Ok(())
251}
252
253async fn test_themes(
254 manifest: &ExtensionManifest,
255 extension_path: &Path,
256 fs: Arc<dyn Fs>,
257) -> Result<()> {
258 for relative_theme_path in &manifest.themes {
259 let theme_path = extension_path.join(relative_theme_path);
260 let theme_family = ThemeRegistry::read_user_theme(&theme_path, fs.clone()).await?;
261 log::info!("loaded theme family {}", theme_family.name);
262 }
263
264 Ok(())
265}