1use std::{
2 borrow::Cow,
3 fs,
4 path::{Path, PathBuf},
5 process::Command,
6 sync::Arc,
7};
8
9use anyhow::{bail, Context, Result};
10use cargo_component_core::{
11 command::CommonOptions,
12 registry::{Dependency, DependencyResolution, DependencyResolver, RegistryResolution},
13};
14use clap::Args;
15use heck::ToKebabCase;
16use semver::VersionReq;
17use toml_edit::{table, value, DocumentMut, Item, Table, Value};
18use wasm_pkg_client::caching::{CachingClient, FileCache};
19
20use crate::{
21 config::Config, generate_bindings, generator::SourceGenerator, load_component_metadata,
22 load_metadata, metadata, metadata::DEFAULT_WIT_DIR, CargoArguments,
23};
24
25const WIT_BINDGEN_RT_CRATE: &str = "wit-bindgen-rt";
26
27fn escape_wit(s: &str) -> Cow<str> {
28 match s {
29 "use" | "type" | "func" | "u8" | "u16" | "u32" | "u64" | "s8" | "s16" | "s32" | "s64"
30 | "float32" | "float64" | "char" | "record" | "flags" | "variant" | "enum" | "union"
31 | "bool" | "string" | "option" | "result" | "future" | "stream" | "list" | "_" | "as"
32 | "from" | "static" | "interface" | "tuple" | "import" | "export" | "world" | "package" => {
33 Cow::Owned(format!("%{s}"))
34 }
35 _ => s.into(),
36 }
37}
38
39#[derive(Args)]
41#[clap(disable_version_flag = true)]
42pub struct NewCommand {
43 #[clap(flatten)]
45 pub common: CommonOptions,
46
47 #[clap(long = "vcs", value_name = "VCS", value_parser = ["git", "hg", "pijul", "fossil", "none"])]
52 pub vcs: Option<String>,
53
54 #[clap(long = "bin", alias = "command", conflicts_with = "lib")]
56 pub bin: bool,
57
58 #[clap(long = "lib", alias = "reactor")]
60 pub lib: bool,
61
62 #[clap(long = "proxy", requires = "lib")]
64 pub proxy: bool,
65
66 #[clap(long = "edition", value_name = "YEAR", value_parser = ["2015", "2018", "2021"])]
68 pub edition: Option<String>,
69
70 #[clap(
72 long = "namespace",
73 value_name = "NAMESPACE",
74 default_value = "component"
75 )]
76 pub namespace: String,
77
78 #[clap(long = "name", value_name = "NAME")]
80 pub name: Option<String>,
81
82 #[clap(long = "editor", value_name = "EDITOR", value_parser = ["emacs", "vscode", "none"])]
84 pub editor: Option<String>,
85
86 #[clap(long = "target", short = 't', value_name = "TARGET", requires = "lib")]
88 pub target: Option<String>,
89
90 #[clap(long = "registry", value_name = "REGISTRY")]
92 pub registry: Option<String>,
93
94 #[clap(long = "no-rustfmt")]
96 pub no_rustfmt: bool,
97
98 #[clap(value_name = "path")]
100 pub path: PathBuf,
101}
102
103struct PackageName<'a> {
104 namespace: String,
105 name: String,
106 display: Cow<'a, str>,
107}
108
109impl<'a> PackageName<'a> {
110 fn new(namespace: &str, name: Option<&'a str>, path: &'a Path) -> Result<Self> {
111 let (name, display) = match name {
112 Some(name) => (name.into(), name.into()),
113 None => (
114 path.file_name().expect("invalid path").to_string_lossy(),
115 path.as_os_str().to_string_lossy(),
118 ),
119 };
120
121 let namespace_kebab = namespace.to_kebab_case();
122 if namespace_kebab.is_empty() {
123 bail!("invalid component namespace `{namespace}`");
124 }
125
126 wit_parser::validate_id(&namespace_kebab).with_context(|| {
127 format!("component namespace `{namespace}` is not a legal WIT identifier")
128 })?;
129
130 let name_kebab = name.to_kebab_case();
131 if name_kebab.is_empty() {
132 bail!("invalid component name `{name}`");
133 }
134
135 wit_parser::validate_id(&name_kebab)
136 .with_context(|| format!("component name `{name}` is not a legal WIT identifier"))?;
137
138 Ok(Self {
139 namespace: namespace_kebab,
140 name: name_kebab,
141 display,
142 })
143 }
144}
145
146impl NewCommand {
147 pub async fn exec(self) -> Result<()> {
149 log::debug!("executing new command");
150
151 let config = Config::new(self.common.new_terminal(), self.common.config.clone()).await?;
152
153 let name = PackageName::new(&self.namespace, self.name.as_deref(), &self.path)?;
154
155 let out_dir = std::env::current_dir()
156 .with_context(|| "couldn't get the current directory of the process")?
157 .join(&self.path);
158
159 let target: Option<metadata::Target> = match self.target.as_deref() {
160 Some(s) if s.contains('@') => Some(s.parse()?),
161 Some(s) => Some(format!("{s}@{version}", version = VersionReq::STAR).parse()?),
162 None => None,
163 };
164 let client = config.client(self.common.cache_dir.clone(), false).await?;
165 let target = self.resolve_target(Arc::clone(&client), target).await?;
166 let source = self.generate_source(&target).await?;
167
168 let mut command = self.new_command();
169 match command.status() {
170 Ok(status) => {
171 if !status.success() {
172 std::process::exit(status.code().unwrap_or(1));
173 }
174 }
175 Err(e) => {
176 bail!("failed to execute `cargo new` command: {e}")
177 }
178 }
179
180 let target = target.map(|(res, world)| {
181 match res {
182 DependencyResolution::Registry(reg) => (reg, world),
183 _ => unreachable!(),
186 }
187 });
188 self.update_manifest(&config, &name, &out_dir, &target)?;
189 self.create_source_file(&config, &out_dir, source.as_ref(), &target)?;
190 self.create_targets_file(&name, &out_dir)?;
191 self.create_editor_settings_file(&out_dir)?;
192
193 let cargo_args = CargoArguments::parse()?;
196 let manifest_path = out_dir.join("Cargo.toml");
197 let metadata = load_metadata(Some(&manifest_path))?;
198 let packages =
199 load_component_metadata(&metadata, cargo_args.packages.iter(), cargo_args.workspace)?;
200 let _import_name_map =
201 generate_bindings(client, &config, &metadata, &packages, &cargo_args).await?;
202
203 Ok(())
204 }
205
206 fn new_command(&self) -> Command {
207 let mut command = std::process::Command::new("cargo");
208 command.arg("new");
209
210 if let Some(name) = &self.name {
211 command.arg("--name").arg(name);
212 }
213
214 if let Some(edition) = &self.edition {
215 command.arg("--edition").arg(edition);
216 }
217
218 if let Some(vcs) = &self.vcs {
219 command.arg("--vcs").arg(vcs);
220 }
221
222 if self.common.quiet {
223 command.arg("-q");
224 }
225
226 command.args(std::iter::repeat("-v").take(self.common.verbose as usize));
227
228 if let Some(color) = self.common.color {
229 command.arg("--color").arg(color.to_string());
230 }
231
232 if !self.is_command() {
233 command.arg("--lib");
234 }
235
236 command.arg(&self.path);
237 command
238 }
239
240 fn update_manifest(
241 &self,
242 config: &Config,
243 name: &PackageName,
244 out_dir: &Path,
245 target: &Option<(RegistryResolution, Option<String>)>,
246 ) -> Result<()> {
247 let manifest_path = out_dir.join("Cargo.toml");
248 let manifest = fs::read_to_string(&manifest_path).with_context(|| {
249 format!(
250 "failed to read manifest file `{path}`",
251 path = manifest_path.display()
252 )
253 })?;
254
255 let mut doc: DocumentMut = manifest.parse().with_context(|| {
256 format!(
257 "failed to parse manifest file `{path}`",
258 path = manifest_path.display()
259 )
260 })?;
261
262 if !self.is_command() {
263 doc["lib"] = table();
264 doc["lib"]["crate-type"] = value(Value::from_iter(["cdylib"]));
265 }
266
267 let mut component = Table::new();
268 component.set_implicit(true);
269
270 component["package"] = value(format!(
271 "{ns}:{name}",
272 ns = name.namespace,
273 name = name.name
274 ));
275
276 if !self.is_command() {
277 if let Some((resolution, world)) = target.as_ref() {
278 let version = if !resolution.requirement.comparators.is_empty()
280 && resolution.requirement.comparators[0].op == semver::Op::Exact
281 {
282 format!("={}", resolution.version)
283 } else {
284 format!("{}", resolution.version)
285 };
286 component["target"] = match world {
287 Some(world) => {
288 value(format!("{name}/{world}@{version}", name = resolution.name,))
289 }
290 None => value(format!("{name}@{version}", name = resolution.name,)),
291 };
292 }
293 }
294
295 component["dependencies"] = Item::Table(Table::new());
296
297 if self.proxy {
298 component["proxy"] = value(true);
299 }
300
301 let mut metadata = Table::new();
302 metadata.set_implicit(true);
303 metadata.set_position(doc.len());
304 metadata["component"] = Item::Table(component);
305 doc["package"]["metadata"] = Item::Table(metadata);
306
307 fs::write(&manifest_path, doc.to_string()).with_context(|| {
308 format!(
309 "failed to write manifest file `{path}`",
310 path = manifest_path.display()
311 )
312 })?;
313
314 let mut cargo_add_command = std::process::Command::new("cargo");
316 cargo_add_command.arg("add");
317 cargo_add_command.arg("--quiet");
318 cargo_add_command.arg(WIT_BINDGEN_RT_CRATE);
319 cargo_add_command.arg("--features");
320 cargo_add_command.arg("bitflags");
321 cargo_add_command.current_dir(out_dir);
322 let status = cargo_add_command
323 .status()
324 .context("failed to execute `cargo add` command")?;
325 if !status.success() {
326 bail!("`cargo add {WIT_BINDGEN_RT_CRATE} --features bitflags` command exited with non-zero status");
327 }
328
329 config.terminal().status(
330 "Updated",
331 format!("manifest of package `{name}`", name = name.display),
332 )?;
333
334 Ok(())
335 }
336
337 fn is_command(&self) -> bool {
338 self.bin || !self.lib
339 }
340
341 async fn generate_source(
342 &self,
343 target: &Option<(DependencyResolution, Option<String>)>,
344 ) -> Result<Cow<str>> {
345 match target {
346 Some((resolution, world)) => {
347 let generator =
348 SourceGenerator::new(resolution, resolution.name(), !self.no_rustfmt);
349 generator.generate(world.as_deref()).await.map(Into::into)
350 }
351 None => {
352 if self.is_command() {
353 Ok(r#"fn main() {
354 println!("Hello, world!");
355}
356"#
357 .into())
358 } else {
359 Ok(r#"#[allow(warnings)]
360mod bindings;
361
362use bindings::Guest;
363
364struct Component;
365
366impl Guest for Component {
367 /// Say hello!
368 fn hello_world() -> String {
369 "Hello, World!".to_string()
370 }
371}
372
373bindings::export!(Component with_types_in bindings);
374"#
375 .into())
376 }
377 }
378 }
379 }
380
381 fn create_source_file(
382 &self,
383 config: &Config,
384 out_dir: &Path,
385 source: &str,
386 target: &Option<(RegistryResolution, Option<String>)>,
387 ) -> Result<()> {
388 let path = if self.is_command() {
389 "src/main.rs"
390 } else {
391 "src/lib.rs"
392 };
393
394 let source_path = out_dir.join(path);
395 fs::write(&source_path, source).with_context(|| {
396 format!(
397 "failed to write source file `{path}`",
398 path = source_path.display()
399 )
400 })?;
401
402 match target {
403 Some((resolution, _)) => {
404 config.terminal().status(
405 "Generated",
406 format!(
407 "source file `{path}` for target `{name}` v{version}",
408 name = resolution.name,
409 version = resolution.version
410 ),
411 )?;
412 }
413 None => {
414 config
415 .terminal()
416 .status("Generated", format!("source file `{path}`"))?;
417 }
418 }
419
420 Ok(())
421 }
422
423 fn create_targets_file(&self, name: &PackageName, out_dir: &Path) -> Result<()> {
424 if self.is_command() || self.target.is_some() {
425 return Ok(());
426 }
427
428 let wit_path = out_dir.join(DEFAULT_WIT_DIR);
429 fs::create_dir(&wit_path).with_context(|| {
430 format!(
431 "failed to create targets directory `{wit_path}`",
432 wit_path = wit_path.display()
433 )
434 })?;
435
436 let path = wit_path.join("world.wit");
437
438 fs::write(
439 &path,
440 format!(
441 r#"package {ns}:{pkg};
442
443/// An example world for the component to target.
444world example {{
445 export hello-world: func() -> string;
446}}
447"#,
448 ns = escape_wit(&name.namespace),
449 pkg = escape_wit(&name.name),
450 ),
451 )
452 .with_context(|| {
453 format!(
454 "failed to write targets file `{path}`",
455 path = path.display()
456 )
457 })
458 }
459
460 fn create_editor_settings_file(&self, out_dir: &Path) -> Result<()> {
461 match self.editor.as_deref() {
462 Some("vscode") | None => {
463 let settings_dir = out_dir.join(".vscode");
464 let settings_path = settings_dir.join("settings.json");
465
466 fs::create_dir_all(settings_dir)?;
467
468 fs::write(
469 &settings_path,
470 r#"{
471 "rust-analyzer.check.overrideCommand": [
472 "cargo",
473 "component",
474 "check",
475 "--workspace",
476 "--all-targets",
477 "--message-format=json"
478 ],
479}
480"#,
481 )
482 .with_context(|| {
483 format!(
484 "failed to write editor settings file `{path}`",
485 path = settings_path.display()
486 )
487 })
488 }
489 Some("emacs") => {
490 let settings_path = out_dir.join(".dir-locals.el");
491
492 fs::create_dir_all(out_dir)?;
493
494 fs::write(
495 &settings_path,
496 r#";;; Directory Local Variables
497;;; For more information see (info "(emacs) Directory Variables")
498
499((lsp-mode . ((lsp-rust-analyzer-cargo-watch-args . ["check"
500 (\, "--message-format=json")])
501 (lsp-rust-analyzer-cargo-watch-command . "component")
502 (lsp-rust-analyzer-cargo-override-command . ["cargo"
503 (\, "component")
504 (\, "check")
505 (\, "--workspace")
506 (\, "--all-targets")
507 (\, "--message-format=json")]))))
508"#,
509 )
510 .with_context(|| {
511 format!(
512 "failed to write editor settings file `{path}`",
513 path = settings_path.display()
514 )
515 })
516 }
517 Some("none") => Ok(()),
518 _ => unreachable!(),
519 }
520 }
521
522 async fn resolve_target(
525 &self,
526 client: Arc<CachingClient<FileCache>>,
527 target: Option<metadata::Target>,
528 ) -> Result<Option<(DependencyResolution, Option<String>)>> {
529 match target {
530 Some(metadata::Target::Package {
531 name,
532 package,
533 world,
534 }) => {
535 let mut resolver = DependencyResolver::new_with_client(client, None)?;
536 let dependency = Dependency::Package(package);
537
538 resolver.add_dependency(&name, &dependency).await?;
539
540 let dependencies = resolver.resolve().await?;
541 assert_eq!(dependencies.len(), 1);
542
543 Ok(Some((
544 dependencies
545 .into_values()
546 .next()
547 .expect("expected a target resolution"),
548 world,
549 )))
550 }
551 _ => Ok(None),
552 }
553 }
554}