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 release_profile = table();
269 release_profile["codegen-units"] = value(1);
270 release_profile["opt-level"] = value("s");
271 release_profile["debug"] = value(false);
272 release_profile["strip"] = value(true);
273 release_profile["lto"] = value(true);
274 let mut profile = table();
275 profile.as_table_mut().unwrap().set_implicit(true);
276 profile["release"] = release_profile;
277 doc["profile"] = profile;
278
279 let mut component = Table::new();
280 component.set_implicit(true);
281
282 component["package"] = value(format!(
283 "{ns}:{name}",
284 ns = name.namespace,
285 name = name.name
286 ));
287
288 if !self.is_command() {
289 if let Some((resolution, world)) = target.as_ref() {
290 let version = if !resolution.requirement.comparators.is_empty()
292 && resolution.requirement.comparators[0].op == semver::Op::Exact
293 {
294 format!("={}", resolution.version)
295 } else {
296 format!("{}", resolution.version)
297 };
298 component["target"] = match world {
299 Some(world) => {
300 value(format!("{name}/{world}@{version}", name = resolution.name,))
301 }
302 None => value(format!("{name}@{version}", name = resolution.name,)),
303 };
304 }
305 }
306
307 component["dependencies"] = Item::Table(Table::new());
308
309 if self.proxy {
310 component["proxy"] = value(true);
311 }
312
313 let mut metadata = Table::new();
314 metadata.set_implicit(true);
315 metadata.set_position(doc.len());
316 metadata["component"] = Item::Table(component);
317 doc["package"]["metadata"] = Item::Table(metadata);
318
319 fs::write(&manifest_path, doc.to_string()).with_context(|| {
320 format!(
321 "failed to write manifest file `{path}`",
322 path = manifest_path.display()
323 )
324 })?;
325
326 let mut cargo_add_command = std::process::Command::new("cargo");
328 cargo_add_command.arg("add");
329 cargo_add_command.arg("--quiet");
330 cargo_add_command.arg(WIT_BINDGEN_RT_CRATE);
331 cargo_add_command.arg("--features");
332 cargo_add_command.arg("bitflags");
333 cargo_add_command.current_dir(out_dir);
334 let status = cargo_add_command
335 .status()
336 .context("failed to execute `cargo add` command")?;
337 if !status.success() {
338 bail!("`cargo add {WIT_BINDGEN_RT_CRATE} --features bitflags` command exited with non-zero status");
339 }
340
341 config.terminal().status(
342 "Updated",
343 format!("manifest of package `{name}`", name = name.display),
344 )?;
345
346 Ok(())
347 }
348
349 fn is_command(&self) -> bool {
350 self.bin || !self.lib
351 }
352
353 async fn generate_source(
354 &self,
355 target: &Option<(DependencyResolution, Option<String>)>,
356 ) -> Result<Cow<str>> {
357 match target {
358 Some((resolution, world)) => {
359 let generator =
360 SourceGenerator::new(resolution, resolution.name(), !self.no_rustfmt);
361 generator.generate(world.as_deref()).await.map(Into::into)
362 }
363 None => {
364 if self.is_command() {
365 Ok(r#"fn main() {
366 println!("Hello, world!");
367}
368"#
369 .into())
370 } else {
371 Ok(r#"#[allow(warnings)]
372mod bindings;
373
374use bindings::Guest;
375
376struct Component;
377
378impl Guest for Component {
379 /// Say hello!
380 fn hello_world() -> String {
381 "Hello, World!".to_string()
382 }
383}
384
385bindings::export!(Component with_types_in bindings);
386"#
387 .into())
388 }
389 }
390 }
391 }
392
393 fn create_source_file(
394 &self,
395 config: &Config,
396 out_dir: &Path,
397 source: &str,
398 target: &Option<(RegistryResolution, Option<String>)>,
399 ) -> Result<()> {
400 let path = if self.is_command() {
401 "src/main.rs"
402 } else {
403 "src/lib.rs"
404 };
405
406 let source_path = out_dir.join(path);
407 fs::write(&source_path, source).with_context(|| {
408 format!(
409 "failed to write source file `{path}`",
410 path = source_path.display()
411 )
412 })?;
413
414 match target {
415 Some((resolution, _)) => {
416 config.terminal().status(
417 "Generated",
418 format!(
419 "source file `{path}` for target `{name}` v{version}",
420 name = resolution.name,
421 version = resolution.version
422 ),
423 )?;
424 }
425 None => {
426 config
427 .terminal()
428 .status("Generated", format!("source file `{path}`"))?;
429 }
430 }
431
432 Ok(())
433 }
434
435 fn create_targets_file(&self, name: &PackageName, out_dir: &Path) -> Result<()> {
436 if self.is_command() || self.target.is_some() {
437 return Ok(());
438 }
439
440 let wit_path = out_dir.join(DEFAULT_WIT_DIR);
441 fs::create_dir(&wit_path).with_context(|| {
442 format!(
443 "failed to create targets directory `{wit_path}`",
444 wit_path = wit_path.display()
445 )
446 })?;
447
448 let path = wit_path.join("world.wit");
449
450 fs::write(
451 &path,
452 format!(
453 r#"package {ns}:{pkg};
454
455/// An example world for the component to target.
456world example {{
457 export hello-world: func() -> string;
458}}
459"#,
460 ns = escape_wit(&name.namespace),
461 pkg = escape_wit(&name.name),
462 ),
463 )
464 .with_context(|| {
465 format!(
466 "failed to write targets file `{path}`",
467 path = path.display()
468 )
469 })
470 }
471
472 fn create_editor_settings_file(&self, out_dir: &Path) -> Result<()> {
473 match self.editor.as_deref() {
474 Some("vscode") | None => {
475 let settings_dir = out_dir.join(".vscode");
476 let settings_path = settings_dir.join("settings.json");
477
478 fs::create_dir_all(settings_dir)?;
479
480 fs::write(
481 &settings_path,
482 r#"{
483 "rust-analyzer.check.overrideCommand": [
484 "cargo",
485 "component",
486 "check",
487 "--workspace",
488 "--all-targets",
489 "--message-format=json"
490 ],
491}
492"#,
493 )
494 .with_context(|| {
495 format!(
496 "failed to write editor settings file `{path}`",
497 path = settings_path.display()
498 )
499 })
500 }
501 Some("emacs") => {
502 let settings_path = out_dir.join(".dir-locals.el");
503
504 fs::create_dir_all(out_dir)?;
505
506 fs::write(
507 &settings_path,
508 r#";;; Directory Local Variables
509;;; For more information see (info "(emacs) Directory Variables")
510
511((lsp-mode . ((lsp-rust-analyzer-cargo-watch-args . ["check"
512 (\, "--message-format=json")])
513 (lsp-rust-analyzer-cargo-watch-command . "component")
514 (lsp-rust-analyzer-cargo-override-command . ["cargo"
515 (\, "component")
516 (\, "check")
517 (\, "--workspace")
518 (\, "--all-targets")
519 (\, "--message-format=json")]))))
520"#,
521 )
522 .with_context(|| {
523 format!(
524 "failed to write editor settings file `{path}`",
525 path = settings_path.display()
526 )
527 })
528 }
529 Some("none") => Ok(()),
530 _ => unreachable!(),
531 }
532 }
533
534 async fn resolve_target(
537 &self,
538 client: Arc<CachingClient<FileCache>>,
539 target: Option<metadata::Target>,
540 ) -> Result<Option<(DependencyResolution, Option<String>)>> {
541 match target {
542 Some(metadata::Target::Package {
543 name,
544 package,
545 world,
546 }) => {
547 let mut resolver = DependencyResolver::new_with_client(client, None)?;
548 let dependency = Dependency::Package(package);
549
550 resolver.add_dependency(&name, &dependency).await?;
551
552 let dependencies = resolver.resolve().await?;
553 assert_eq!(dependencies.len(), 1);
554
555 Ok(Some((
556 dependencies
557 .into_values()
558 .next()
559 .expect("expected a target resolution"),
560 world,
561 )))
562 }
563 _ => Ok(None),
564 }
565 }
566}