From 6f1b72e547ae4e365174e5a8db47f87bd8751dd9 Mon Sep 17 00:00:00 2001 From: jerj Date: Wed, 27 Feb 2019 16:26:41 -0500 Subject: [PATCH] Refactor into a library, use structopt for CLI handling (#172) --- Cargo.toml | 1 + src/build.rs | 175 +++++++++--------- src/cargo_shim/mod.rs | 24 ++- src/cmd_build.rs | 13 +- src/cmd_deploy.rs | 6 +- src/cmd_start.rs | 23 +-- src/cmd_test.rs | 21 +-- src/lib.rs | 385 ++++++++++++++++++++++++++++++++++++++ src/main.rs | 417 +++--------------------------------------- src/wasm_runtime.rs | 17 ++ 10 files changed, 560 insertions(+), 522 deletions(-) create mode 100644 src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 0f3b008..6009a27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ http = "0.1.13" futures = "0.1.17" open = "1" failure = "0.1" +structopt = "0.2.14" [dependencies.semver] features = ["serde"] diff --git a/src/build.rs b/src/build.rs index 11e50bb..9a5bc25 100644 --- a/src/build.rs +++ b/src/build.rs @@ -1,9 +1,9 @@ use std::collections::HashMap; use std::process::{Command, Stdio, exit}; use std::path::{Path, PathBuf}; +use std::str::FromStr; use std::env; -use clap; use cargo_shim::{ Profile, CargoPackage, @@ -29,6 +29,10 @@ use wasm; use wasm_runtime::RuntimeKind; +const ASMJS_UNKNOWN_EMSCRIPTEN: &str = "asmjs-unknown-emscripten"; +const WASM32_UNKNOWN_EMSCRIPTEN: &str = "wasm32-unknown-emscripten"; +const WASM32_UNKNOWN_UNKNOWN: &str = "wasm32-unknown-unknown"; + #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum PathKind { File, @@ -48,6 +52,22 @@ pub enum Backend { WebAssembly } +impl FromStr for Backend { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + WASM32_UNKNOWN_UNKNOWN => Ok(Backend::WebAssembly), + WASM32_UNKNOWN_EMSCRIPTEN => Ok(Backend::EmscriptenWebAssembly), + ASMJS_UNKNOWN_EMSCRIPTEN => Ok(Backend::EmscriptenAsmJs), + _ => Err(Error::ConfigurationError(format!( + "{} is not a valid target triple.", + s + ))), + } + } +} + impl Backend { pub fn is_emscripten_asmjs( self ) -> bool { self == Backend::EmscriptenAsmJs @@ -71,9 +91,9 @@ impl Backend { pub fn triplet( &self ) -> &str { match *self { - Backend::EmscriptenAsmJs => "asmjs-unknown-emscripten", - Backend::EmscriptenWebAssembly => "wasm32-unknown-emscripten", - Backend::WebAssembly => "wasm32-unknown-unknown" + Backend::EmscriptenAsmJs => ASMJS_UNKNOWN_EMSCRIPTEN, + Backend::EmscriptenWebAssembly => WASM32_UNKNOWN_EMSCRIPTEN, + Backend::WebAssembly => WASM32_UNKNOWN_UNKNOWN, } } } @@ -86,6 +106,28 @@ enum TargetName { Bench( String ) } +impl TargetName { + pub fn from(t: super::Target) -> Option { + if t.lib { + return Some(TargetName::Lib); + } + + if let Some(bin) = t.bin { + return Some(TargetName::Bin(bin)); + } + + if let Some(example) = t.example { + return Some(TargetName::Example(example)); + } + + if let Some(bench) = t.bench { + return Some(TargetName::Bench(bench)); + } + + None + } +} + #[derive(Clone)] pub struct BuildArgs { features: Vec< String >, @@ -112,95 +154,50 @@ pub struct AggregatedConfig { pub prepend_js: Vec< (PathBuf, String) > } -impl BuildArgs { - pub fn new( matches: &clap::ArgMatches ) -> Result< Self, Error > { - let features = if let Some( features ) = matches.value_of( "features" ) { - features.split_whitespace().map( |feature| feature.to_owned() ).collect() - } else { - Vec::new() - }; - - let no_default_features = matches.is_present( "no-default-features" ); - let enable_all_features = matches.is_present( "all-features" ); - - let build_type = if matches.is_present( "release" ) { - BuildType::Release - } else { - BuildType::Debug - }; - - let use_system_emscripten = matches.is_present( "use-system-emscripten" ); - let is_verbose = matches.is_present( "verbose" ); - let message_format = if let Some( name ) = matches.value_of( "message-format" ) { - match name { - "human" => MessageFormat::Human, - "json" => MessageFormat::Json, - _ => unreachable!() - } - } else { - MessageFormat::Human - }; - - let backend = if matches.is_present( "target-webasm-emscripten" ) { - eprintln!( "warning: `--target-webasm-emscripten` argument is deprecated; please use `--target wasm32-unknown-emscripten` instead" ); - Some( Backend::EmscriptenWebAssembly ) - } else if matches.is_present( "target-webasm" ) { - eprintln!( "warning: `--target-webasm` argument is deprecated; please use `--target wasm32-unknown-unknown` instead" ); - Some( Backend::WebAssembly ) - } else if matches.is_present( "target-asmjs-emscripten" ) { - eprintln!( "warning: `--target-asmjs-emscripten` argument is deprecated; please use `--target asmjs-unknown-emscripten` instead" ); - Some( Backend::EmscriptenAsmJs ) - } else if let Some( triplet ) = matches.value_of( "target" ) { - let backend = match triplet { - "asmjs-unknown-emscripten" => Backend::EmscriptenAsmJs, - "wasm32-unknown-emscripten" => Backend::EmscriptenWebAssembly, - "wasm32-unknown-unknown" => Backend::WebAssembly, - _ => unreachable!( "Unknown target: {:?}", triplet ) - }; - - Some( backend ) - } else { - None - }; +impl From for BuildArgs { + fn from(b: super::Build) -> Self { + Self { + features: b.features.unwrap_or_default().split(' ').map(String::from).collect(), + no_default_features: b.no_default_features, + enable_all_features: b.all_features, + build_type: if b.release { BuildType::Release } else { BuildType::Debug }, + use_system_emscripten: b.use_system_emscripten, + is_verbose: b.verbose, + message_format: MessageFormat::Human, + backend: b.target, + runtime: RuntimeKind::Standalone, + package_name: b.package, + target_name: None, + } + } +} - let runtime = if let Some( runtime ) = matches.value_of( "runtime" ) { - match runtime { - "standalone" => RuntimeKind::Standalone, - "library-es6" => RuntimeKind::LibraryEs6, - "web-extension" => RuntimeKind::WebExtension, - "experimental-only-loader" => RuntimeKind::OnlyLoader, - _ => unreachable!( "Unknown runtime: {:?}", runtime ) +impl BuildArgs { + pub(crate) fn new(build: super::Build, ext: super::BuildExt, target: super::Target) -> Result { + let mut out = Self::from(build).with_target(target); + out.message_format = ext.message_format; + + if let Some(rt) = ext.runtime { + match out.backend { + None | Some(Backend::WebAssembly) => { + out.runtime = rt; + } + Some(be) => { + return Err(Error::ConfigurationError(format!( + "JavaScript runtime can only be specified for target `{}`. (Current target is `{}`)", + WASM32_UNKNOWN_UNKNOWN, + be.triplet() + ))); + } } - } else { - RuntimeKind::Standalone - }; + } - let package_name = matches.value_of( "package" ).map( |name| name.to_owned() ); - let target_name = if matches.is_present( "lib" ) { - Some( TargetName::Lib ) - } else if let Some( name ) = matches.value_of( "bin" ) { - Some( TargetName::Bin( name.to_owned() ) ) - } else if let Some( name ) = matches.value_of( "example" ) { - Some( TargetName::Example( name.to_owned() ) ) - } else if let Some( name ) = matches.value_of( "bench" ) { - Some( TargetName::Bench( name.to_owned() ) ) - } else { - None - }; + Ok(out) + } - Ok( BuildArgs { - features, - no_default_features, - enable_all_features, - build_type, - use_system_emscripten, - is_verbose, - message_format, - backend, - runtime, - package_name, - target_name - }) + pub(crate) fn with_target(mut self, target: super::Target) -> Self { + self.target_name = TargetName::from(target); + self } pub fn load_project( &self ) -> Result< Project, Error > { diff --git a/src/cargo_shim/mod.rs b/src/cargo_shim/mod.rs index fe9699d..824ed98 100644 --- a/src/cargo_shim/mod.rs +++ b/src/cargo_shim/mod.rs @@ -8,7 +8,7 @@ use std::ops::Deref; use std::cell::Cell; use std::env; use std::thread; -use std::str; +use std::str::{self, FromStr}; use std::error; use std::fmt; @@ -525,7 +525,24 @@ impl BuildTarget { #[derive(Copy, Clone, PartialEq, Debug)] pub enum MessageFormat { Human, - Json + Json, + #[doc(hidden)] + __Nonexhaustive, +} + +impl FromStr for MessageFormat { + type Err = super::Error; + + fn from_str(s: &str) -> Result { + match s { + "human" => Ok(MessageFormat::Human), + "json" => Ok(MessageFormat::Json), + _ => Err(super::Error::ConfigurationError(format!( + "{} is not a valid message format. Possible values are `human` and `json`.", + s + ))), + } + } } #[derive(Clone, Debug)] @@ -778,6 +795,7 @@ impl BuildConfig { MessageFormat::Json => { println!( "{}", serde_json::to_string( &message.to_json_value() ).unwrap() ); } + MessageFormat::__Nonexhaustive => unreachable!(), } }, CargoOutput::Artifact( artifact ) => { @@ -793,6 +811,7 @@ impl BuildConfig { MessageFormat::Json => { println!( "{}", serde_json::to_string( &executed.to_json_value() ).unwrap() ); } + MessageFormat::__Nonexhaustive => unreachable!(), } } } @@ -877,6 +896,7 @@ impl BuildConfig { MessageFormat::Json => { println!( "{}", serde_json::to_string( &artifact.to_json_value() ).unwrap() ); } + MessageFormat::__Nonexhaustive => unreachable!(), } for filename in artifact.filenames { diff --git a/src/cmd_build.rs b/src/cmd_build.rs index b9e4d25..f284c4b 100644 --- a/src/cmd_build.rs +++ b/src/cmd_build.rs @@ -1,5 +1,3 @@ -use clap; - use cargo_shim::{ Profile, TargetKind @@ -8,8 +6,7 @@ use cargo_shim::{ use build::BuildArgs; use error::Error; -fn command_build_or_check< 'a >( matches: &clap::ArgMatches< 'a >, should_build: bool ) -> Result< (), Error > { - let build_args = BuildArgs::new( matches )?; +pub fn command_build_or_check(build_args: BuildArgs, should_build: bool) -> Result<(), Error> { let project = build_args.load_project()?; let targets = project.target_or_select( |target| { @@ -28,10 +25,10 @@ fn command_build_or_check< 'a >( matches: &clap::ArgMatches< 'a >, should_build: Ok(()) } -pub fn command_build< 'a >( matches: &clap::ArgMatches< 'a > ) -> Result< (), Error > { - command_build_or_check( matches, true ) +pub fn command_build(args: BuildArgs) -> Result<(), Error> { + command_build_or_check(args, true) } -pub fn command_check< 'a >( matches: &clap::ArgMatches< 'a > ) -> Result< (), Error > { - command_build_or_check( matches, false ) +pub fn command_check(args: BuildArgs) -> Result<(), Error> { + command_build_or_check(args, false) } diff --git a/src/cmd_deploy.rs b/src/cmd_deploy.rs index 647ee5b..5433561 100644 --- a/src/cmd_deploy.rs +++ b/src/cmd_deploy.rs @@ -1,8 +1,6 @@ use std::fs; use std::path::PathBuf; -use clap; - use cargo_shim::{ Profile, TargetKind @@ -12,8 +10,7 @@ use build::BuildArgs; use deployment::Deployment; use error::Error; -pub fn command_deploy< 'a >( matches: &clap::ArgMatches< 'a > ) -> Result< (), Error > { - let build_args = BuildArgs::new( matches )?; +pub fn command_deploy(build_args: BuildArgs, directory: Option) -> Result<(), Error> { let project = build_args.load_project()?; let package = project.package(); @@ -35,7 +32,6 @@ pub fn command_deploy< 'a >( matches: &clap::ArgMatches< 'a > ) -> Result< (), E let result = project.build( &config, target )?; let deployment = Deployment::new( package, target, &result )?; - let directory = matches.value_of( "output" ).map( PathBuf::from ); let is_using_default_directory; let directory = match directory { diff --git a/src/cmd_start.rs b/src/cmd_start.rs index 9064883..82aea50 100644 --- a/src/cmd_start.rs +++ b/src/cmd_start.rs @@ -3,7 +3,7 @@ use std::sync::mpsc::channel; use std::sync::{Mutex, Arc}; use std::time::{Instant, Duration}; use std::thread; -use std::net::{self, ToSocketAddrs}; +use std::net; use std::hash::Hash; use std::time::{SystemTime, UNIX_EPOCH}; use std::path::{Path, PathBuf}; @@ -15,7 +15,6 @@ use notify::{ DebouncedEvent }; -use clap; use handlebars::Handlebars; use percent_encoding::percent_decode; @@ -253,15 +252,13 @@ fn monitor_for_changes_and_rebuild( watcher } -fn address_or_default< 'a >( matches: &clap::ArgMatches< 'a > ) -> net::SocketAddr { - let host = matches.value_of( "host" ).unwrap_or( "localhost" ); - let port = matches.value_of( "port" ).unwrap_or( "8000" ); - format!( "{}:{}", host, port ).to_socket_addrs().unwrap().next().unwrap() -} - -pub fn command_start< 'a >( matches: &clap::ArgMatches< 'a > ) -> Result< (), Error > { - let auto_reload = matches.is_present( "auto-reload" ); - let build_args = BuildArgs::new( matches )?; +pub fn command_start( + build_args: BuildArgs, + host: net::IpAddr, + port: u16, + open: bool, + auto_reload: bool +) -> Result<(), Error> { let project = build_args.load_project()?; let last_build = { @@ -280,7 +277,7 @@ pub fn command_start< 'a >( matches: &clap::ArgMatches< 'a > ) -> Result< (), Er let _watcher = monitor_for_changes_and_rebuild( last_build.clone() ); let target_name = target.name.clone(); - let address = address_or_default( matches ); + let address = net::SocketAddr::new(host, port); let server = SimpleServer::new(&address, move |request| { let path = request.uri().path(); let path = percent_decode( path.as_bytes() ).decode_utf8().unwrap(); @@ -341,7 +338,7 @@ pub fn command_start< 'a >( matches: &clap::ArgMatches< 'a > ) -> Result< (), Er eprintln!( "" ); eprintln!( "You can access the web server at `http://{}`.", &address ); - if matches.is_present( "open" ) { + if open { thread::spawn( move || { // Wait for server to start let start = Instant::now(); diff --git a/src/cmd_test.rs b/src/cmd_test.rs index eb5441d..9153afc 100644 --- a/src/cmd_test.rs +++ b/src/cmd_test.rs @@ -4,8 +4,6 @@ use std::env; use std::fs; use std::ffi::OsStr; -use clap; - use cargo_shim::{ Profile, CargoResult, @@ -30,8 +28,7 @@ fn test_in_nodejs( build: CargoResult, arg_passthrough: &Vec< &OsStr >, any_failure: &mut bool -) -> Result< (), Error > { - +) -> Result<(), Error> { let possible_commands = if cfg!( windows ) { &[ "node.exe" ][..] @@ -68,7 +65,7 @@ fn test_in_nodejs( }; let test_args = iter::once( runner_path.as_os_str() ) - .chain( iter::once( OsStr::new( backend.triplet() ) ) ) + .chain( iter::once( OsStr::new(backend.triplet()) ) ) .chain( iter::once( artifact.as_os_str() ) ) .chain( arg_passthrough.iter().cloned() ); @@ -98,16 +95,14 @@ fn test_in_nodejs( Ok(()) } -pub fn command_test< 'a >( matches: &clap::ArgMatches< 'a > ) -> Result< (), Error > { - let build_args = BuildArgs::new( matches )?; +pub fn command_test<'a>( + build_args: BuildArgs, + use_nodejs: bool, + no_run: bool, + arg_passthrough: &Vec<&OsStr>, +) -> Result<(), Error> { let project = build_args.load_project()?; - let use_nodejs = matches.is_present( "nodejs" ); - let no_run = matches.is_present( "no-run" ); - - let arg_passthrough = matches.values_of_os( "passthrough" ) - .map_or( vec![], |args| args.collect() ); - let targets = project.target_or_select( |target| { target.kind == TargetKind::Lib || target.kind == TargetKind::CDyLib || diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..49b71f8 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,385 @@ +//! A `cargo` subcommand for the client-side Web. + +#![deny( + missing_debug_implementations, + trivial_numeric_casts, + unstable_features, + unused_import_braces, + unused_qualifications +)] + +#[macro_use] +extern crate structopt; +extern crate clap; +extern crate digest; +extern crate futures; +extern crate http; +extern crate hyper; +extern crate libflate; +extern crate notify; +extern crate pbr; +extern crate reqwest; +extern crate serde; +extern crate sha1; +extern crate sha2; +extern crate tar; +extern crate tempfile; +extern crate toml; +#[macro_use] +extern crate serde_derive; +#[macro_use] +extern crate serde_json; +extern crate base_x; +extern crate handlebars; +extern crate indexmap; +extern crate regex; +extern crate unicode_categories; +extern crate walkdir; +extern crate websocket; +#[macro_use] +extern crate lazy_static; +extern crate directories; +extern crate percent_encoding; + +extern crate parity_wasm; +#[macro_use] +extern crate log; +extern crate env_logger; +extern crate rustc_demangle; + +extern crate ansi_term; +extern crate cargo_metadata; + +extern crate memmap; +extern crate semver; + +extern crate atty; +extern crate open; +#[macro_use] +extern crate failure; + +mod cargo_shim; + +#[macro_use] +mod utils; +mod build; +mod chrome_devtools; +mod cmd_build; +mod cmd_deploy; +mod cmd_prepare_emscripten; +mod cmd_start; +mod cmd_test; +mod config; +mod deployment; +mod emscripten; +mod error; +mod http_utils; +mod package; +mod project_dirs; +mod test_chromium; +mod wasm; +mod wasm_context; +mod wasm_export_main; +mod wasm_export_table; +mod wasm_gc; +mod wasm_hook_grow; +mod wasm_inline_js; +mod wasm_intrinsics; +mod wasm_js_export; +mod wasm_js_snippet; +mod wasm_runtime; + +use std::ffi::OsStr; +use std::net::{IpAddr, ToSocketAddrs}; +use std::path::PathBuf; + +use build::{Backend, BuildArgs}; +use cargo_shim::MessageFormat; +use error::Error; +use wasm_runtime::RuntimeKind; + +/// CLI for `cargo-web` +#[derive(Debug, StructOpt)] +#[structopt(name = "cargo-web")] +#[structopt(about = "A `cargo` subcommand for the client-side web.")] +#[structopt(raw(global_setting = "structopt::clap::AppSettings::ColoredHelp"))] +#[structopt(raw(setting = "structopt::clap::AppSettings::VersionlessSubcommands"))] +#[structopt(rename_all = "kebab-case")] +pub enum CargoWebOpts { + /// Compile a local package and all of its dependencies + Build(BuildOpts), + /// Typecheck a local package and all of its dependencies + Check(CheckOpts), + /// Deploys your project so that it's ready to be served statically + Deploy(DeployOpts), + /// Fetches and installs prebuilt Emscripten packages + PrepareEmscripten(PrepareEmscriptenOpts), + /// Runs an embedded web server, which serves the built project + Start(StartOpts), + /// Compiles and runs tests + Test(TestOpts), + #[doc(hidden)] + #[structopt(raw(setting = "structopt::clap::AppSettings::Hidden"))] + __Nonexhaustive, +} + +/// Run a subcommand based on a configuration +pub fn run(cfg: CargoWebOpts) -> Result<(), Error> { + match cfg { + CargoWebOpts::Build(BuildOpts { + build_args, + build_target, + ext, + }) => cmd_build::command_build(BuildArgs::new(build_args, ext, build_target)?), + CargoWebOpts::Check(CheckOpts { + build_args, + build_target, + ext, + }) => cmd_build::command_check(BuildArgs::new(build_args, ext, build_target)?), + CargoWebOpts::Deploy(DeployOpts { build_args, output }) => { + cmd_deploy::command_deploy(build_args.into(), output) + } + CargoWebOpts::PrepareEmscripten(_) => cmd_prepare_emscripten::command_prepare_emscripten(), + CargoWebOpts::Start(StartOpts { + build_args, + build_target, + auto_reload, + open, + port, + host, + }) => cmd_start::command_start( + BuildArgs::from(build_args).with_target(build_target), + host, + port, + open, + auto_reload, + ), + CargoWebOpts::Test(TestOpts { + build_args, + nodejs, + no_run, + passthrough, + }) => { + let pass_os = passthrough.iter().map(OsStr::new).collect::>(); + cmd_test::command_test(build_args.into(), nodejs, no_run, &pass_os) + } + CargoWebOpts::__Nonexhaustive => unreachable!(), + } +} + +/// Options for `cargo web build` +#[derive(Debug, StructOpt)] +#[structopt(rename_all = "kebab-case")] +pub struct BuildOpts { + #[structopt(flatten)] + build_args: Build, + #[structopt(flatten)] + ext: BuildExt, + #[structopt(flatten)] + build_target: Target, +} + +/// Options for `cargo web check` +#[derive(Debug, StructOpt)] +#[structopt(rename_all = "kebab-case")] +pub struct CheckOpts { + #[structopt(flatten)] + build_args: Build, + #[structopt(flatten)] + ext: BuildExt, + #[structopt(flatten)] + build_target: Target, +} + +/// Options for `cargo web deploy` +#[derive(Debug, StructOpt)] +#[structopt(rename_all = "kebab-case")] +pub struct DeployOpts { + /// Output directory; the default is `$CARGO_TARGET_DIR/deploy` + #[structopt(short = "o", long, parse(from_os_str))] + output: Option, + #[structopt(flatten)] + build_args: Build, +} + +/// Options for `cargo web prepare-emscripten` +#[derive(Debug, StructOpt)] +#[structopt(rename_all = "kebab-case")] +pub struct PrepareEmscriptenOpts { + #[doc(hidden)] + #[structopt(raw(set = "structopt::clap::ArgSettings::Hidden"))] + __reserved: bool, +} + +/// Options for `cargo web start` +#[derive(Debug, StructOpt)] +#[structopt(rename_all = "kebab-case")] +pub struct StartOpts { + /// Bind the server to this address + #[structopt( + long, + parse(try_from_str = "resolve_host"), + default_value = "localhost" + )] + host: IpAddr, + /// Bind the server to this port + #[structopt(long, default_value = "8000")] + port: u16, + /// Open browser after server starts + #[structopt(long)] + open: bool, + /// Will try to automatically reload the page on rebuild + #[structopt(long)] + auto_reload: bool, + #[structopt(flatten)] + build_target: Target, + #[structopt(flatten)] + build_args: Build, +} + +/// Options for `cargo web test` +#[derive(Debug, StructOpt)] +#[structopt(rename_all = "kebab-case")] +pub struct TestOpts { + /// Compile, but don't run tests + #[structopt(long)] + no_run: bool, + /// Uses Node.js to run the tests + #[structopt(long)] + nodejs: bool, + #[structopt(flatten)] + build_args: Build, + /// all additional arguments will be passed through to the test runner + passthrough: Vec, +} + +/// Select a target to build +#[derive(Debug, StructOpt)] +#[structopt(rename_all = "kebab-case")] +struct Target { + /// Build only this package's library + #[structopt(long, group = "target_type")] + lib: bool, + /// Build only the specified binary + #[structopt(long, group = "target_type")] + bin: Option, + /// Build only the specified example + #[structopt(long, group = "target_type")] + example: Option, + /// Build only the specified test target + #[structopt(long, group = "target_type")] + test: Option, + /// Build only the specified benchmark target + #[structopt(long, group = "target_type")] + bench: Option, +} + +impl Default for Target { + fn default() -> Self { + Self { + lib: false, + bin: None, + example: None, + test: None, + bench: None, + } + } +} + +/// Specify additional build options +#[derive(Debug, StructOpt)] +#[structopt(rename_all = "kebab-case")] +struct BuildExt { + /// Selects the stdout output format + #[structopt( + long, + default_value = "human", + parse(try_from_str), + raw(possible_values = "&[\"human\", \"json\"]"), + raw(set = "structopt::clap::ArgSettings::NextLineHelp") + )] + message_format: MessageFormat, + /// Selects the type of JavaScript runtime which will be generated + /// (Only valid when targeting `wasm32-unknown-unknown`). [possible values: + /// standalone, library-es6, web-extension] + #[structopt( + long, + parse(try_from_str), + raw(set = "structopt::clap::ArgSettings::NextLineHelp") + )] + runtime: Option, +} + +impl Default for BuildExt { + fn default() -> Self { + Self { + message_format: MessageFormat::Json, + runtime: None, + } + } +} + +/// Build configuration for one or more targets +#[derive(Debug, StructOpt)] +#[structopt(rename_all = "kebab-case")] +struct Build { + /// Package to build + #[structopt(short = "p", long)] + pub package: Option, + /// Additional features to build (space-delimited list) + #[structopt(long, group = "build_features")] + pub features: Option, + /// Build all available features + #[structopt(long, group = "build_features")] + pub all_features: bool, + /// Do not build the `default` feature + #[structopt(long, group = "build_features")] + pub no_default_features: bool, + /// Won't try to download Emscripten; will always use the system one + #[structopt(long)] + pub use_system_emscripten: bool, + /// Build artifacts in release mode, with optimizations + #[structopt(long)] + pub release: bool, + /// Target triple to build for, overriding setting in `Web.toml`. If not + /// specified in `Web.toml`, default target is `wasm32-unknown-unknown`. + #[structopt( + long, + parse(try_from_str), + raw( + possible_values = "&[\"wasm32-unknown-unknown\", \"wasm32-unknown-emscripten\", \"asmjs-unknown-emscripten\"]" + ), + raw(set = "structopt::clap::ArgSettings::NextLineHelp") + )] + pub target: Option, + /// Use verbose output + #[structopt(short = "v", long)] + pub verbose: bool, +} + +impl Default for Build { + /// Returns a sensible default config. + /// + /// # Note + /// If you want to change the target triple, use `Into`, e.g. + /// `target: "asmjs-unknown-emscripten".into()` + fn default() -> Self { + Self { + package: None, + features: None, + all_features: false, + no_default_features: false, + use_system_emscripten: false, + release: false, + target: None, + verbose: false, + } + } +} + +/// Resolve hostname to IP address +fn resolve_host(host: &str) -> std::io::Result { + (host, 0) + .to_socket_addrs() + .map(|itr| itr.map(|a| a.ip()).collect::>()[0]) +} diff --git a/src/main.rs b/src/main.rs index e1b1569..1e02787 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,410 +1,43 @@ -#![deny( - missing_debug_implementations, - trivial_numeric_casts, - unstable_features, - unused_import_braces, - unused_qualifications -)] - -extern crate clap; -extern crate notify; -extern crate hyper; -extern crate http; -extern crate futures; -extern crate tempfile; -extern crate reqwest; -extern crate pbr; -extern crate libflate; -extern crate tar; -extern crate sha1; -extern crate sha2; -extern crate digest; -extern crate toml; -extern crate serde; -#[macro_use] -extern crate serde_derive; -#[macro_use] -extern crate serde_json; -extern crate handlebars; -extern crate unicode_categories; -extern crate indexmap; -extern crate websocket; -extern crate regex; -extern crate walkdir; -extern crate base_x; -#[macro_use] -extern crate lazy_static; -extern crate directories; -extern crate percent_encoding; - -extern crate parity_wasm; -#[macro_use] -extern crate log; -extern crate rustc_demangle; +extern crate cargo_web; extern crate env_logger; +extern crate structopt; -extern crate cargo_metadata; -extern crate ansi_term; - -extern crate semver; -extern crate memmap; - -extern crate open; -extern crate atty; -#[macro_use] -extern crate failure; - +use std::env::{args, var}; use std::process::exit; -use std::env; -use clap::{ - Arg, - App, - AppSettings, - SubCommand -}; +use cargo_web::{run, CargoWebOpts}; +use structopt::StructOpt; -mod cargo_shim; +macro_rules! target_arg { + ( $opt:expr, $arch:ident $abi:ident ) => {{ + let triple = concat!(stringify!($arch), "-unknown-", stringify!($abi)); -#[macro_use] -mod utils; -mod project_dirs; -mod http_utils; -mod config; -mod package; -mod build; -mod deployment; -mod error; -mod wasm; -mod wasm_gc; -mod wasm_inline_js; -mod wasm_export_main; -mod wasm_export_table; -mod wasm_hook_grow; -mod wasm_runtime; -mod wasm_context; -mod wasm_intrinsics; -mod wasm_js_export; -mod wasm_js_snippet; -mod emscripten; -mod test_chromium; -mod chrome_devtools; -mod cmd_build; -mod cmd_start; -mod cmd_test; -mod cmd_deploy; -mod cmd_prepare_emscripten; - -fn add_shared_build_params< 'a, 'b >( app: App< 'a, 'b > ) -> App< 'a, 'b > { - return app - .arg( - Arg::with_name( "package" ) - .short( "p" ) - .long( "package" ) - .help( "Package to build" ) - .value_name( "NAME" ) - .takes_value( true ) - ) - .arg( - Arg::with_name( "features" ) - .long( "features" ) - .help( "Space-separated list of features to also build" ) - .value_name( "FEATURES" ) - .takes_value( true ) - ) - .arg( - Arg::with_name( "all-features" ) - .long( "all-features" ) - .help( "Build all available features" ) - // Technically Cargo doesn't treat it as conflicting, - // but it seems less confusing to *not* allow these together. - .conflicts_with_all( &[ "features", "no-default-features" ] ) - ) - .arg( - Arg::with_name( "no-default-features" ) - .long( "no-default-features" ) - .help( "Do not build the `default` feature" ) - ) - .arg( - Arg::with_name( "use-system-emscripten" ) - .long( "use-system-emscripten" ) - .help( "Won't try to download Emscripten; will always use the system one" ) - ) - .arg( - Arg::with_name( "release" ) - .long( "release" ) - .help( "Build artifacts in release mode, with optimizations" ) - ) - .arg( - Arg::with_name( "target" ) - .long( "target" ) - .takes_value( true ) - .value_name( "TRIPLE" ) - .help( "Build for the target [default: wasm32-unknown-unknown]" ) - .possible_values( &[ "asmjs-unknown-emscripten", "wasm32-unknown-emscripten", "wasm32-unknown-unknown" ] ) - .conflicts_with_all( &["target-asmjs-emscripten", "target-webasm-emscripten", "target-webasm"] ) - ) - .arg( - Arg::with_name( "verbose" ) - .short( "v" ) - .long( "verbose" ) - .help( "Use verbose output" ) - ) - // These three are legacy options kept for compatibility. - .arg( - Arg::with_name( "target-asmjs-emscripten" ) - .long( "target-asmjs-emscripten" ) - .help( "Generate asmjs through Emscripten (default)" ) - .overrides_with_all( &["target-webasm-emscripten", "target-webasm"] ) - .hidden( true ) - ) - .arg( - Arg::with_name( "target-webasm-emscripten" ) - .long( "target-webasm-emscripten" ) - .help( "Generate webasm through Emscripten" ) - .overrides_with_all( &["target-asmjs-emscripten", "target-webasm"] ) - .hidden( true ) - ) - .arg( - Arg::with_name( "target-webasm" ) - .long( "target-webasm" ) - .help( "Generates webasm through Rust's native backend (HIGHLY EXPERIMENTAL!)" ) - .overrides_with_all( &["target-asmjs-emscripten", "target-webasm-emscripten"] ) - .hidden( true ) + eprintln!( + "The `--target-{}` flag is DEPRECATED. Please use the `--target` \ + option with the full triple (`{}`)", + $opt, triple ); -} -fn add_build_params< 'a, 'b >( app: App< 'a, 'b > ) -> App< 'a, 'b > { - return app - .arg( - Arg::with_name( "lib" ) - .long( "lib" ) - .help( "Build only this package's library" ) - ) - .arg( - Arg::with_name( "bin" ) - .long( "bin" ) - .help( "Build only the specified binary" ) - .value_name( "NAME" ) - .takes_value( true ) - ) - .arg( - Arg::with_name( "example" ) - .long( "example" ) - .help( "Build only the specified example" ) - .value_name( "NAME" ) - .takes_value( true ) - ) - .arg( - Arg::with_name( "test" ) - .long( "test" ) - .help( "Build only the specified test target" ) - .value_name( "NAME" ) - .takes_value( true ) - ) - .arg( - Arg::with_name( "bench" ) - .long( "bench" ) - .help( "Build only the specified benchmark target" ) - .value_name( "NAME" ) - .takes_value( true ) - ) - .arg( - Arg::with_name( "message-format" ) - .long( "message-format" ) - .help( "Selects the stdout output format" ) - .value_name( "FMT" ) - .takes_value( true ) - .default_value( "human" ) - .possible_values( &[ - "human", - "json" - ]) - ) - .arg( - Arg::with_name( "runtime" ) - .long( "runtime" ) - .takes_value( true ) - .value_name( "RUNTIME" ) - .help( "Selects the type of JavaScript runtime which will be generated; valid only for the `wasm32-unknown-unknown` target [possible values: standalone, library-es6, web-extension]" ) - .possible_values( &["standalone", "library-es6", "web-extension", "experimental-only-loader"] ) - .default_value_if( - "target", Some( "wasm32-unknown-unknown" ), - "standalone" - ) - .hide_possible_values( true ) // Get rid of this after removing `experimental-only-loader` variant. - ); + format!("--target={}", triple) + }}; } fn main() { - if let Ok( value ) = env::var( "CARGO_WEB_LOG" ) { + if let Ok(value) = var("CARGO_WEB_LOG") { let mut builder = env_logger::Builder::new(); - builder.parse( &value ); + builder.parse(&value); builder.init(); } - let args = { - // To allow running both as 'cargo-web' and as 'cargo web'. - let mut args = env::args(); - let mut filtered_args = Vec::new(); - filtered_args.push( args.next().unwrap() ); - - match args.next() { - None => {}, - #[cfg(any(unix))] - Some( ref arg ) if filtered_args[ 0 ].ends_with( "cargo-web" ) && arg == "web" => {}, - #[cfg(any(windows))] - Some( ref arg ) if filtered_args[ 0 ].ends_with( "cargo-web.exe" ) && arg == "web" => {}, - Some( arg ) => filtered_args.push( arg ) - } - - filtered_args.extend( args ); - filtered_args - }; - - let mut build_subcommand = - SubCommand::with_name( "build" ) - .about( "Compile a local package and all of its dependencies" ); - - let mut check_subcommand = - SubCommand::with_name( "check" ) - .about( "Typecheck a local package and all of its dependencies" ); - - build_subcommand = add_build_params( build_subcommand ); - check_subcommand = add_build_params( check_subcommand ); - - let mut test_subcommand = - SubCommand::with_name( "test" ) - .about( "Compiles and runs tests" ) - .arg( - Arg::with_name( "no-run" ) - .long( "no-run" ) - .help( "Compile, but don't run tests" ) - ) - .arg( - Arg::with_name( "nodejs" ) - .long( "nodejs" ) - .help( "Uses Node.js to run the tests" ) - ) - .arg( - Arg::with_name( "passthrough" ) - .help( "-- followed by anything will pass the arguments to the test runner") - .multiple( true ) - .takes_value( true ) - .last( true ) - ); - - let mut start_subcommand = - SubCommand::with_name( "start" ) - .about( "Runs an embedded web server serving the built project" ) - .arg( - Arg::with_name( "bin" ) - .long( "bin" ) - .help( "Build only the specified binary" ) - .value_name( "NAME" ) - .takes_value( true ) - ) - .arg( - Arg::with_name( "example" ) - .long( "example" ) - .help( "Serves the specified example" ) - .value_name( "NAME" ) - .takes_value( true ) - ) - .arg( - Arg::with_name( "test" ) - .long( "test" ) - .help( "Build only the specified test target" ) - .value_name( "NAME" ) - .takes_value( true ) - ) - .arg( - Arg::with_name( "bench" ) - .long( "bench" ) - .help( "Build only the specified benchmark target" ) - .value_name( "NAME" ) - .takes_value( true ) - ) - .arg( - Arg::with_name( "host" ) - .long( "host" ) - .help( "Bind the server to this address, default `localhost`") - .value_name( "HOST" ) - .takes_value( true ) - ) - .arg( - Arg::with_name( "port" ) - .long( "port" ) - .help( "Bind the server to this port, default 8000" ) - .value_name( "PORT" ) - .takes_value( true ) - ) - .arg( - Arg::with_name( "open" ) - .long( "open" ) - .help( "Open browser after server starts" ) - ) - .arg( - Arg::with_name( "auto-reload" ) - .long( "auto-reload" ) - .help( "Will try to automatically reload the page on rebuild" ) - ); - - let mut deploy_subcommand = - SubCommand::with_name( "deploy" ) - .about( "Deploys your project so that its ready to be served statically" ) - .arg( - Arg::with_name( "output" ) - .short( "o" ) - .long( "output" ) - .help( "Output directory; the default is `$CARGO_TARGET_DIR/deploy`") - .value_name( "OUTPUT_DIRECTORY" ) - .takes_value( true ) - ); - - let prepare_emscripten_subcommand = - SubCommand::with_name( "prepare-emscripten" ) - .about( "Fetches and installs prebuilt Emscripten packages" ); - - build_subcommand = add_shared_build_params( build_subcommand ); - check_subcommand = add_shared_build_params( check_subcommand ); - test_subcommand = add_shared_build_params( test_subcommand ); - start_subcommand = add_shared_build_params( start_subcommand ); - deploy_subcommand = add_shared_build_params( deploy_subcommand ); - - let matches = App::new( "cargo-web" ) - .version( env!( "CARGO_PKG_VERSION" ) ) - .setting( AppSettings::SubcommandRequiredElseHelp ) - .setting( AppSettings::VersionlessSubcommands ) - .subcommand( build_subcommand ) - .subcommand( check_subcommand ) - .subcommand( test_subcommand ) - .subcommand( start_subcommand ) - .subcommand( deploy_subcommand ) - .subcommand( prepare_emscripten_subcommand ) - .get_matches_from( args ); - - let result = if let Some( matches ) = matches.subcommand_matches( "build" ) { - cmd_build::command_build( matches ) - } else if let Some( matches ) = matches.subcommand_matches( "check" ) { - cmd_build::command_check( matches ) - } else if let Some( matches ) = matches.subcommand_matches( "test" ) { - cmd_test::command_test( matches ) - } else if let Some( matches ) = matches.subcommand_matches( "start" ) { - cmd_start::command_start( matches ) - } else if let Some( matches ) = matches.subcommand_matches( "deploy" ) { - cmd_deploy::command_deploy( matches ) - } else if let Some( _ ) = matches.subcommand_matches( "prepare-emscripten" ) { - cmd_prepare_emscripten::command_prepare_emscripten() - } else { - return; - }; + let argv = args().into_iter().map(|arg| match arg.as_ref() { + "--target-webasm" => target_arg!("webasm", wasm32 unknown), + "--target-webasm-emscripten" => target_arg!("webasm-emscripten", wasm32 emscripten), + "--target-asmjs-emscripten" => target_arg!("asmjs-emscripten", asmjs emscripten), + _ => arg, + }); - match result { - Ok( _ ) => {}, - Err( error ) => { - eprintln!( "error: {}", error ); - exit( 101 ); - } + if let Err(error) = run(CargoWebOpts::from_iter(argv)) { + eprintln!("error: {}", error); + exit(101); } } diff --git a/src/wasm_runtime.rs b/src/wasm_runtime.rs index 229aa39..64f35cb 100644 --- a/src/wasm_runtime.rs +++ b/src/wasm_runtime.rs @@ -2,6 +2,7 @@ use std::path::Path; use std::collections::BTreeMap; use std::fmt::Write as FmtWrite; use std::fmt::Display; +use std::str::FromStr; use unicode_categories::UnicodeCategories; use handlebars::Handlebars; @@ -9,6 +10,8 @@ use handlebars::Handlebars; use wasm_inline_js::JsSnippet; use wasm_js_export::{JsExport, TypeMetadata}; +use super::Error; + #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum RuntimeKind { Standalone, @@ -17,6 +20,20 @@ pub enum RuntimeKind { OnlyLoader } +impl FromStr for RuntimeKind { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "standalone" => Ok(RuntimeKind::Standalone), + "library-es6" => Ok(RuntimeKind::LibraryEs6), + "web-extension" => Ok(RuntimeKind::WebExtension), + "experimental-only-loader" => Ok(RuntimeKind::OnlyLoader), + _ => Err(Error::ConfigurationError(format!("{} is not a valid runtime type.", s))), + } + } +} + // This is probably a total overkill, but oh well. fn to_js_identifier( string: &str ) -> String { // Source: https://mathiasbynens.be/notes/javascript-identifiers