1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
use clap::Parser;
use itertools::Itertools;
use std::{
collections::HashSet,
env,
ffi::OsStr,
fmt::Debug,
fs, io,
path::{self, Path, PathBuf},
process::{Command, ExitStatus, Stdio},
};
use cargo_metadata::{Metadata, MetadataCommand, Package};
/// Build a contract from source
///
/// Builds all crates that are referenced by the cargo manifest (Cargo.toml)
/// that have cdylib as their crate-type. Crates are built for the wasm32
/// target. Unless configured otherwise, crates are built with their default
/// features and with their release profile.
///
/// In workspaces builds all crates unless a package name is specified, or the
/// command is executed from the sub-directory of a workspace crate.
///
/// To view the commands that will be executed, without executing them, use the
/// --print-commands-only option.
#[derive(Parser, Debug, Clone)]
pub struct Cmd {
/// Path to Cargo.toml
#[arg(long)]
pub manifest_path: Option<std::path::PathBuf>,
/// Package to build
///
/// If omitted, all packages that build for crate-type cdylib are built.
#[arg(long)]
pub package: Option<String>,
/// Build with the specified profile
#[arg(long, default_value = "release")]
pub profile: String,
/// Build with the list of features activated, space or comma separated
#[arg(long, help_heading = "Features")]
pub features: Option<String>,
/// Build with the all features activated
#[arg(
long,
conflicts_with = "features",
conflicts_with = "no_default_features",
help_heading = "Features"
)]
pub all_features: bool,
/// Build with the default feature not activated
#[arg(long, help_heading = "Features")]
pub no_default_features: bool,
/// Directory to copy wasm files to
///
/// If provided, wasm files can be found in the cargo target directory, and
/// the specified directory.
///
/// If ommitted, wasm files are written only to the cargo target directory.
#[arg(long)]
pub out_dir: Option<std::path::PathBuf>,
/// Print commands to build without executing them
#[arg(long, conflicts_with = "out_dir", help_heading = "Other")]
pub print_commands_only: bool,
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error(transparent)]
Metadata(#[from] cargo_metadata::Error),
#[error(transparent)]
CargoCmd(io::Error),
#[error("exit status {0}")]
Exit(ExitStatus),
#[error("package {package} not found")]
PackageNotFound { package: String },
#[error("finding absolute path of Cargo.toml: {0}")]
AbsolutePath(io::Error),
#[error("creating out directory: {0}")]
CreatingOutDir(io::Error),
#[error("copying wasm file: {0}")]
CopyingWasmFile(io::Error),
#[error("getting the current directory: {0}")]
GettingCurrentDir(io::Error),
}
impl Cmd {
pub fn run(&self) -> Result<(), Error> {
let working_dir = env::current_dir().map_err(Error::GettingCurrentDir)?;
let metadata = self.metadata()?;
let packages = self.packages(&metadata)?;
let target_dir = &metadata.target_directory;
if let Some(package) = &self.package {
if packages.is_empty() {
return Err(Error::PackageNotFound {
package: package.clone(),
});
}
}
for p in packages {
let mut cmd = Command::new("cargo");
cmd.stdout(Stdio::piped());
cmd.arg("rustc");
let manifest_path = pathdiff::diff_paths(&p.manifest_path, &working_dir)
.unwrap_or(p.manifest_path.clone().into());
cmd.arg(format!(
"--manifest-path={}",
manifest_path.to_string_lossy()
));
cmd.arg("--crate-type=cdylib");
cmd.arg("--target=wasm32-unknown-unknown");
if self.profile == "release" {
cmd.arg("--release");
} else {
cmd.arg(format!("--profile={}", self.profile));
}
if self.all_features {
cmd.arg("--all-features");
}
if self.no_default_features {
cmd.arg("--no-default-features");
}
if let Some(features) = self.features() {
let requested: HashSet<String> = features.iter().cloned().collect();
let available = p.features.iter().map(|f| f.0).cloned().collect();
let activate = requested.intersection(&available).join(",");
if !activate.is_empty() {
cmd.arg(format!("--features={activate}"));
}
}
let cmd_str = format!(
"cargo {}",
cmd.get_args().map(OsStr::to_string_lossy).join(" ")
);
if self.print_commands_only {
println!("{cmd_str}");
} else {
eprintln!("{cmd_str}");
let status = cmd.status().map_err(Error::CargoCmd)?;
if !status.success() {
return Err(Error::Exit(status));
}
if let Some(out_dir) = &self.out_dir {
fs::create_dir_all(out_dir).map_err(Error::CreatingOutDir)?;
let file = format!("{}.wasm", p.name.replace('-', "_"));
let target_file_path = Path::new(target_dir)
.join("wasm32-unknown-unknown")
.join(&self.profile)
.join(&file);
let out_file_path = Path::new(out_dir).join(&file);
fs::copy(target_file_path, out_file_path).map_err(Error::CopyingWasmFile)?;
}
}
}
Ok(())
}
fn features(&self) -> Option<Vec<String>> {
self.features
.as_ref()
.map(|f| f.split(&[',', ' ']).map(String::from).collect())
}
fn packages(&self, metadata: &Metadata) -> Result<Vec<Package>, Error> {
// Filter by the package name if one is provided, or by the package that
// matches the manifest path if the manifest path matches a specific
// package.
let name = if let Some(name) = self.package.clone() {
Some(name)
} else {
// When matching a package based on the manifest path, match against the
// absolute path because the paths in the metadata are absolute. Match
// against a manifest in the current working directory if no manifest is
// specified.
let manifest_path = path::absolute(
self.manifest_path
.clone()
.unwrap_or(PathBuf::from("Cargo.toml")),
)
.map_err(Error::AbsolutePath)?;
metadata
.packages
.iter()
.find(|p| p.manifest_path == manifest_path)
.map(|p| p.name.clone())
};
let packages = metadata
.packages
.iter()
.filter(|p|
// Filter by the package name if one is selected based on the above logic.
if let Some(name) = &name {
&p.name == name
} else {
// Otherwise filter crates that are default members of the
// workspace and that build to cdylib (wasm).
metadata.workspace_default_members.contains(&p.id)
&& p.targets
.iter()
.any(|t| t.crate_types.iter().any(|c| c == "cdylib"))
}
)
.cloned()
.collect();
Ok(packages)
}
fn metadata(&self) -> Result<Metadata, cargo_metadata::Error> {
let mut cmd = MetadataCommand::new();
cmd.no_deps();
// Set the manifest path if one is provided, otherwise rely on the cargo
// commands default behavior of finding the nearest Cargo.toml in the
// current directory, or the parent directories above it.
if let Some(manifest_path) = &self.manifest_path {
cmd.manifest_path(manifest_path);
}
// Do not configure features on the metadata command, because we are
// only collecting non-dependency metadata, features have no impact on
// the output.
cmd.exec()
}
}