1use crate::config;
5use crate::data::wax_index;
6use crate::dataflow::find_command_result::FindCommandResult;
7use crate::dataflow::installed_packages::{InstalledPackages, RegistryInstaller};
8use crate::dataflow::lockfile_packages::{LockfilePackages, LockfileResult};
9use crate::dataflow::merged_lockfile_packages::MergedLockfilePackages;
10use crate::dataflow::resolved_packages::ResolvedPackages;
11use crate::dataflow::retained_lockfile_packages::RetainedLockfilePackages;
12use crate::dataflow::WapmPackageKey;
13use crate::graphql::{execute_query, DateTime};
14use crate::util;
16use anyhow::Context;
17use thiserror::Error;
18
19use graphql_client::*;
20
21use std::convert::From;
22use std::ffi::OsString;
23use std::path::PathBuf;
24use std::str::FromStr;
25use structopt::{clap::AppSettings, StructOpt};
26
27#[derive(StructOpt, Debug)]
28pub enum ExecuteOpt {
29 #[structopt(external_subcommand)]
31 ExecArgs(Vec<String>),
32}
33
34impl ExecuteOpt {
35 fn args(&self) -> &[String] {
36 match self {
37 ExecuteOpt::ExecArgs(args) => args.as_slice(),
38 }
39 }
40}
41
42#[derive(StructOpt, Debug, Clone, Default)]
45#[structopt(settings = &[AppSettings::ColoredHelp])]
46struct ExecuteOptInner {
47 #[structopt(long = "emscripten")]
49 enable_emscripten: bool,
50
51 #[structopt(long = "force-yes", short = "y")]
53 force_yes: bool,
54
55 #[structopt(long = "verify", short = "v")]
57 verify_signature: bool,
58
59 #[structopt(long = "offline")]
61 offline: bool,
62
63 #[structopt(
65 long = "dir",
66 multiple = true,
67 group = "wasi",
68 value_name = "DIRECTORY"
69 )]
70 pre_opened_directories: Vec<String>,
71
72 #[structopt(long = "no-default-preopen")]
74 no_default_preopen: bool,
75
76 #[structopt(long = "which", value_name = "COMMAND")]
78 which: Option<String>,
79
80 #[structopt(conflicts_with_all(&["which"]), index = 1, value_name = "COMMAND")]
82 command: Option<String>,
83
84 #[structopt(multiple = true, parse(from_os_str), last(true), value_name = "ARGS")]
86 args: Vec<OsString>,
87}
88
89impl ExecuteOptInner {
90 fn print_help_text() {
91 ExecuteOptInner::clap()
94 .bin_name("wax")
95 .usage("wax [FLAGS] [OPTIONS] <COMMAND> [<ARGS>...]")
96 .print_help()
97 .expect("could not print help text");
98 println!();
100 }
101}
102
103#[derive(Debug, Error)]
104enum ExecuteError {
105 #[error(
106 "Command `{0}` not found in the registry {registry:?} or in the current directory",
107 name
108 )]
109 CommandNotFound { name: String, registry: String },
110 #[error(
111 "The command `{0}` is using the Emscripten ABI which may be implmented in a way that is partially unsandbooxed. To opt-in to executing Emscripten Wasm modules run the command again with the `--emscripten` flag",
112 name
113 )]
114 EmscriptenDisabled { name: String },
115 #[error("Error with the Wax index: {0}")]
116 WaxIndexError(wax_index::WaxIndexError),
117 #[error("Error parsing data from register: {0}")]
118 ErrorInDataFromRegistry(String),
119 #[error("An error occured during installation: {0}")]
120 InstallationError(String),
121 #[error("Please specify a command to run.")]
122 NoCommandGiven,
123 #[error(
124 "The command `{0}` was not found.\nIf you are offline you will need to reconnect to the Internet for this command to succeed.\nOtherwise there may be an error with the wapm registry that you're connected to: run `wapm config get registry.url` to print the URL that we tried to connect to.",
125 )]
126 CommandNotFoundOfflineMode(String),
127 #[error(
128 "The command `{0}` was not found.\nPlease try to run this command again without the `--offline` flag.",
129 )]
130 CommandNotFoundOfflineModeOfflineFlag(String),
131}
132
133#[derive(Debug, Error)]
134enum ExecuteArgParsingError {
135 #[error("Argument `{arg_name}` expects a value `{expected}` but none was found.")]
136 MissingValue { arg_name: String, expected: String },
137 #[error("Unrecognized argument `{arg_name}`")]
138 UnrecognizedArgument { arg_name: String },
139}
140
141#[derive(GraphQLQuery)]
142#[graphql(
143 schema_path = "graphql/schema.graphql",
144 query_path = "graphql/queries/wax_get_command.graphql",
145 response_derives = "Debug"
146)]
147struct WaxGetCommandQuery;
148
149#[derive(GraphQLQuery)]
150#[graphql(
151 schema_path = "graphql/schema.graphql",
152 query_path = "graphql/queries/wax_get_package_command.graphql",
153 response_derives = "Debug"
154)]
155struct WaxGetPackageCommandQuery;
156
157use crate::commands::add::get_package_version_query;
158
159use super::add::GetPackageVersionQuery;
160
161#[derive(Debug)]
162struct QueriedPackage {
163 package: String,
164 command_to_exec: String,
165 version: semver::Version,
166 download_url: String,
167}
168
169impl QueriedPackage {
170 pub fn query(opt: &ExecuteOptInner, package_name: &str) -> Option<Self> {
171 if opt.offline {
172 return None;
173 }
174
175 let split = package_name.split('@').collect::<Vec<_>>();
176 let (mut package_name, mut version) = match split.as_slice() {
177 [n, v] => (n.to_string(), Some(v.to_string())),
178 [n] => (n.to_string(), None),
179 _ => {
180 return None;
181 }
182 };
183
184 let command_name;
185 if let Some(v) = version.clone() {
186 let split2 = v.split(':').collect::<Vec<_>>();
187 let (v, c) = match split2.as_slice() {
188 [n, v] => (n.to_string(), Some(v.to_string())),
189 [n] => (n.to_string(), None),
190 _ => {
191 return None;
192 }
193 };
194 version = Some(v);
195 command_name = c;
196 } else {
197 let split2 = package_name.split(':').collect::<Vec<_>>();
198 let (v, c) = match split2.as_slice() {
199 [n, v] => (n.to_string(), Some(v.to_string())),
200 [n] => (n.to_string(), None),
201 _ => {
202 return None;
203 }
204 };
205 package_name = v;
206 command_name = c;
207 }
208
209 match version {
210 None => {
211 let q2 = WaxGetPackageCommandQuery::build_query(
212 wax_get_package_command_query::Variables { name: package_name },
213 );
214
215 let response: wax_get_package_command_query::ResponseData =
216 execute_query(&q2).ok()?;
217 let download_url = response
218 .package
219 .as_ref()?
220 .last_version
221 .as_ref()?
222 .distribution
223 .download_url
224 .clone();
225 let version = semver::Version::from_str(
226 &response.package.as_ref()?.last_version.as_ref()?.version,
227 )
228 .map_err(|e| ExecuteError::ErrorInDataFromRegistry(e.to_string()))
229 .ok()?;
230 let manifest = wapm_toml::Manifest::parse(
231 &response.package.as_ref()?.last_version.as_ref()?.manifest,
232 )
233 .ok()?;
234 let command_to_exec = match command_name {
235 Some(s) => s,
236 None => manifest
237 .command
238 .unwrap_or_default()
239 .iter()
240 .map(|v| v.get_name())
241 .next()?,
242 };
243 let name = response.package.as_ref()?.name.clone();
244 Some(QueriedPackage {
245 package: name,
246 command_to_exec,
247 version,
248 download_url,
249 })
250 }
251 Some(v) => {
252 let q3 =
253 GetPackageVersionQuery::build_query(get_package_version_query::Variables {
254 name: package_name,
255 version: Some(v),
256 });
257 let response: get_package_version_query::ResponseData = execute_query(&q3).ok()?;
258 let download_url = response
259 .package_version
260 .as_ref()?
261 .distribution
262 .download_url
263 .clone();
264 let version =
265 semver::Version::from_str(&response.package_version.as_ref()?.version)
266 .map_err(|e| ExecuteError::ErrorInDataFromRegistry(e.to_string()))
267 .ok()?;
268 let manifest =
269 wapm_toml::Manifest::parse(&response.package_version.as_ref()?.manifest)
270 .ok()?;
271 let command_to_exec = match command_name {
272 Some(s) => s,
273 None => manifest
274 .command
275 .unwrap_or_default()
276 .iter()
277 .map(|v| v.get_name())
278 .next()?,
279 };
280 let name = response.package_version.as_ref()?.package.name.clone();
281 Some(QueriedPackage {
282 package: name,
283 command_to_exec,
284 version,
285 download_url,
286 })
287 }
288 }
289 }
290}
291
292fn transform_args(arg_stream: &[String]) -> Result<ExecuteOptInner, ExecuteArgParsingError> {
294 let mut idx = 0;
295 let mut out = ExecuteOptInner::default();
296 let parse_which = |which_arg: Option<String>| -> Result<String, ExecuteArgParsingError> {
297 let val: String = which_arg.ok_or_else(|| ExecuteArgParsingError::MissingValue {
298 arg_name: "--which".to_string(),
299 expected: "<COMMAND NAME>".to_string(),
300 })?;
301 Ok(val)
302 };
303 let parse_dir = |dir_arg: Option<String>| -> Result<String, ExecuteArgParsingError> {
304 let val: String = dir_arg.ok_or_else(|| ExecuteArgParsingError::MissingValue {
305 arg_name: "--dir".to_string(),
306 expected: "<DIRECTORY>".to_string(),
307 })?;
308 Ok(val)
309 };
310 while idx < arg_stream.len() {
311 match arg_stream[idx].as_ref() {
312 "--emscripten" => out.enable_emscripten = true,
313 "--force_yes" | "-y" => out.force_yes = true,
314 "--verify" | "-v" => out.verify_signature = true,
315 "--no-default-preopen" => out.no_default_preopen = true,
316 "--offline" => out.offline = true,
317 "--which" => {
318 out.which = Some(parse_which(arg_stream.get(idx + 1).cloned())?);
319 idx += 1;
320 }
321 "--dir" => {
322 out.pre_opened_directories
323 .push(parse_dir(arg_stream.get(idx + 1).cloned())?);
324 idx += 1;
325 }
326 "help" | "--help" | "-h" => {
327 ExecuteOptInner::print_help_text();
328 std::process::exit(0);
329 }
330 misc => {
331 if misc.contains('=') {
332 let mut splitter = misc.split('=');
334 let arg = splitter.next().unwrap();
335 let val = splitter.next().map(|x| x.to_string());
336 match arg {
337 "--which" => out.which = Some(parse_which(val)?),
338 "--dir" => {
339 out.pre_opened_directories.push(parse_dir(val)?);
340 }
341 otherwise => {
342 return Err(ExecuteArgParsingError::UnrecognizedArgument {
343 arg_name: otherwise.to_string(),
344 })
345 }
346 }
347 } else {
348 out.command = Some(misc.to_string());
350 if let Some(after_command) = arg_stream.get(idx + 1) {
351 if after_command == "--" {
353 idx += 1;
354 }
355 }
356 out.args = arg_stream[idx + 1..].iter().map(Into::into).collect();
357 break;
358 }
359 }
360 }
361 idx += 1;
362 }
363
364 Ok(out)
365}
366
367pub fn execute(opt: ExecuteOpt) -> anyhow::Result<()> {
368 let mut opt = transform_args(opt.args())?;
369 if !opt.no_default_preopen {
370 opt.pre_opened_directories.push(".".into());
371 }
372 let opt = opt;
373 trace!("Execute {:?}", &opt);
374 let current_dir = crate::config::Config::get_current_dir()?;
375 let config = crate::config::Config::from_file()?;
376 if let Some(which) = opt.which {
377 let mut wax_index = wax_index::WaxIndex::open()?;
378 let dir = if let Ok((package_name, version, _)) = wax_index.search_for_entry(which.clone())
379 {
380 wax_index
381 .base_path()
382 .join(format!("{}@{}", package_name, version))
383 } else if let FindCommandResult::CommandFound { manifest_dir, .. } =
384 FindCommandResult::find_command_in_directory(¤t_dir, &which)
385 {
386 manifest_dir
387 } else {
388 return Err(ExecuteError::CommandNotFound {
389 name: which,
390 registry: config.registry.get_current_registry(),
391 }
392 .into());
393 };
394
395 println!("{}", dir.to_string_lossy());
396 return Ok(());
397 }
398 let command = if let Some(command) = &opt.command {
399 command.clone()
400 } else {
401 ExecuteOptInner::print_help_text();
402 return Err(ExecuteError::NoCommandGiven.into());
403 };
404 let command_name = command.as_str();
405 let _value = util::set_wapm_should_accept_all_prompts(opt.force_yes);
406 debug_assert!(
407 _value.is_some(),
408 "this function should only be called once!"
409 );
410
411 match FindCommandResult::find_command_in_directory(¤t_dir, command_name) {
413 FindCommandResult::CommandNotFound { .. } => {
414 debug!(
416 "Wax: Command \"{}\" not found locally in directory {}",
417 &command_name,
418 current_dir.to_string_lossy()
419 );
420 }
421 FindCommandResult::CommandFound {
422 source,
423 manifest_dir,
424 args: _,
425 module_name,
426 prehashed_cache_key,
427 } => {
428 debug!(
429 "Wax command found locally in {}",
430 current_dir.to_string_lossy()
431 );
432 crate::commands::run::do_run(
434 current_dir,
435 source,
436 manifest_dir,
437 command_name,
438 &module_name,
439 &opt.pre_opened_directories,
440 &opt.args,
441 prehashed_cache_key,
442 )?;
443 return Ok(());
444 }
445 FindCommandResult::Error(e) => {
446 warn!(
447 "Error in Wax when looking for command locally: {}... Continuing execution",
448 e
449 );
450 }
451 }
452
453 let mut wax_index = wax_index::WaxIndex::open()?;
455 let (wax_info, exists_and_recently_updated): (Option<(String, semver::Version)>, bool) =
456 if let Ok((package_name, version, last_seen)) =
457 wax_index.search_for_entry(command_name.to_string())
458 {
459 let wax_cooldown = get_wax_cooldown();
460 trace!("Using wax cooldown: {}", wax_cooldown);
461 let now = time::now_utc().to_timespec();
462 let cooldown_duration = time::Duration::seconds(wax_cooldown as i64);
463 let time_to_update = last_seen + cooldown_duration;
464
465 let recently_updated = now < time_to_update;
467 debug!("The package was recently updated? {}", recently_updated);
468
469 (Some((package_name, version)), recently_updated)
470 } else {
471 (None, false)
472 };
473
474 if exists_and_recently_updated {
475 let (package_name, version) = wax_info
476 .clone()
477 .expect("critical internal logic error in `wapm execute`");
478 let package_version_str = format!("{}@{}", &package_name, &version);
479 let location = wax_index.base_path().join(&package_version_str);
480 if !location
481 .join("wapm_packages")
482 .join(&package_version_str)
483 .join("wapm.toml")
484 .exists()
485 {
486 debug!("Package found in index, but cache has been cleared! continuing to logic that should reinstall");
487 } else {
488 wax_index.save()?;
490
491 match run(
492 command_name,
493 location,
494 &opt.pre_opened_directories,
495 &opt.args,
496 ) {
497 Ok(()) => return Ok(()),
498 Err(e) => {
499 debug!("Failed to run when cached due to `{}`. continuing...", e);
502 }
503 }
504 }
505 }
506
507 let q = WaxGetCommandQuery::build_query(wax_get_command_query::Variables {
509 command: command_name.to_string(),
510 });
511 let response: wax_get_command_query::ResponseData = if !opt.offline {
512 debug!("Querying server for package info");
513 let response: Result<wax_get_command_query::ResponseData, _> = execute_query(&q);
514 if response.is_err() {
515 info!("Failed to connect to the wapm registry. Continuning in offline mode.");
516 return do_offline_run(command_name, &opt);
517 }
518 response?
519 } else {
520 return do_offline_run(command_name, &opt);
521 };
522
523 trace!("Wax get command query: {:?}", response);
524
525 let mut run_command_name = command_name.to_string();
526 let reg_ver;
527 let package_name;
528 let mut command_is_package = false;
529
530 let (mut install_loc, resolved_packages) = if let Some(command) = response.command {
531 if let Some(abi) = command.module.abi.as_ref() {
533 if abi == "emscripten" && !opt.enable_emscripten {
534 return Err(ExecuteError::EmscriptenDisabled {
535 name: command_name.to_string(),
536 }
537 .into());
538 }
539 }
540 let registry_version = semver::Version::from_str(&command.package_version.version)
541 .map_err(|e| ExecuteError::ErrorInDataFromRegistry(e.to_string()))?;
542
543 if let Some((package_name, version)) = wax_info {
544 let package_version_str = format!("{}@{}", &package_name, &version);
545 let location = wax_index.base_path().join(&package_version_str);
546 if registry_version > version
547 || !location
548 .join("wapm_packages")
549 .join(&package_version_str)
550 .join("wapm.toml")
551 .exists()
552 {
553 debug!(
554 "Found version {} locally in Wax but version {} from the registry: upgrading",
555 version, registry_version
556 );
557 } else {
558 debug!(
559 "Command found in Wax index, executing version {} directly",
560 version
561 );
562 wax_index.insert_entry(
563 command_name.to_string(),
564 registry_version,
565 command.package_version.package.name,
566 );
567 wax_index.save()?;
568
569 run(
570 command_name,
571 location,
572 &opt.pre_opened_directories,
573 &opt.args,
574 )?;
575 return Ok(());
576 }
577 } else {
578 debug!("Entry not found in wax index");
579 }
580
581 trace!("Installing Wax package from registry");
586 let install_loc = wax_index.base_path().join(format!(
587 "{}@{}",
588 &command.package_version.package.name, ®istry_version
589 ));
590
591 reg_ver = Some(registry_version.clone());
592 package_name = Some(command.package_version.package.name.clone());
593
594 (
595 install_loc,
596 ResolvedPackages {
597 packages: vec![(
598 WapmPackageKey {
599 name: command.package_version.package.name.into(),
600 version: registry_version,
601 },
602 (
603 command.package_version.distribution.download_url,
604 None, ),
625 )],
626 },
627 )
628 } else if let Some(package) = QueriedPackage::query(&opt, command_name).as_ref() {
629 command_is_package = true;
630 run_command_name = package.command_to_exec.clone();
631 let install_loc = wax_index
632 .base_path()
633 .join(format!("{}@{}", &package.package, &package.version));
634
635 reg_ver = Some(package.version.clone());
636 package_name = Some(package.package.clone());
637
638 (
639 install_loc,
640 ResolvedPackages {
641 packages: vec![(
642 WapmPackageKey {
643 name: package.package.clone().into(),
644 version: package.version.clone(),
645 },
646 (package.download_url.clone(), None),
647 )],
648 },
649 )
650 } else {
651 return Err(ExecuteError::CommandNotFound {
652 name: command_name.to_string(),
653 registry: config.registry.get_current_registry(),
654 }
655 .into());
656 };
657
658 let registry_version = reg_ver.context("no registry version")?;
659 let package_name = package_name.context("no package name")?;
660
661 let lockfile_result = LockfileResult::find_in_directory(&install_loc);
663 let lockfile_packages = LockfilePackages::new_from_result(lockfile_result)
664 .map_err(|e| ExecuteError::InstallationError(e.to_string()))?;
665 let installed_packages = InstalledPackages::install::<RegistryInstaller>(
666 &install_loc,
667 resolved_packages,
668 !opt.verify_signature,
669 )?;
670 let added_lockfile_data = LockfilePackages::from_installed_packages(&installed_packages)
671 .map_err(|e| ExecuteError::InstallationError(e.to_string()))?;
672
673 let retained_lockfile_packages =
674 RetainedLockfilePackages::from_lockfile_packages(lockfile_packages);
675 let final_lockfile_data =
676 MergedLockfilePackages::merge(added_lockfile_data, retained_lockfile_packages);
677 final_lockfile_data
678 .generate_lockfile(&install_loc)
679 .map_err(|e| ExecuteError::InstallationError(e.to_string()))?;
680
681 debug!("Wax package installed to {}", install_loc.to_string_lossy());
682
683 wax_index.insert_entry(
684 run_command_name.to_string(),
685 registry_version.clone(),
686 package_name.clone(),
687 );
688 wax_index.save()?;
689
690 if command_is_package {
691 let install_loc_original = install_loc.clone();
692 let mut name = package_name.split('/');
693 install_loc = install_loc.join("wapm_packages");
694 if let Some(s) = name.next() {
695 install_loc = install_loc.join(s)
696 }
697 if let Some(s) = name.next() {
698 install_loc = install_loc.join(format!("{s}@{registry_version}"));
699 }
700
701 let _ = std::fs::copy(
702 install_loc_original.join("wapm.lock"),
703 install_loc.clone().join("wapm.lock"),
704 );
705 }
706
707 run(
708 &run_command_name,
709 install_loc,
710 &opt.pre_opened_directories,
711 &opt.args,
712 )?;
713
714 Ok(())
715}
716
717fn run(
718 command_name: &str,
719 location: PathBuf,
720 pre_opened_directories: &[String],
721 args: &[OsString],
722) -> anyhow::Result<()> {
723 match FindCommandResult::find_command_in_directory(&location, command_name) {
724 FindCommandResult::CommandNotFound { error, extended: _ } => {
725 todo!(
729 "Implement command not found logic!: {}, {}: {}",
730 location.to_string_lossy(),
731 &command_name,
732 error,
733 );
734 }
735 FindCommandResult::CommandFound {
736 source,
737 manifest_dir,
738 args: _,
739 module_name,
740 prehashed_cache_key,
741 } => {
742 crate::logging::clear_stdout()?;
743 crate::commands::run::do_run(
744 location,
745 source,
746 manifest_dir,
747 command_name,
748 &module_name,
749 pre_opened_directories,
750 args,
751 prehashed_cache_key,
752 )
753 }
754 FindCommandResult::Error(e) => Err(e),
755 }
756}
757
758fn do_offline_run(command_name: &str, opt: &ExecuteOptInner) -> anyhow::Result<()> {
759 let mut wax_index = wax_index::WaxIndex::open()?;
760 if let Ok((package_name, version, _)) = wax_index.search_for_entry(command_name.to_string()) {
761 let package_version_str = format!("{}@{}", &package_name, &version);
762 let location = wax_index.base_path().join(package_version_str);
763
764 wax_index.save()?;
765
766 crate::logging::clear_stdout()?;
767 run(
768 command_name,
769 location,
770 &opt.pre_opened_directories,
771 &opt.args,
772 )
773 } else if opt.offline {
774 Err(ExecuteError::CommandNotFoundOfflineModeOfflineFlag(command_name.to_string()).into())
775 } else {
776 Err(ExecuteError::CommandNotFoundOfflineMode(command_name.to_string()).into())
777 }
778}
779
780impl From<wax_index::WaxIndexError> for ExecuteError {
781 fn from(other: wax_index::WaxIndexError) -> Self {
782 ExecuteError::WaxIndexError(other)
783 }
784}
785
786fn get_wax_cooldown() -> i32 {
787 config::Config::from_file()
788 .ok()
789 .map(|c| c.wax_cooldown)
790 .unwrap_or(config::wax_default_cooldown())
791}