use clap::{App, Arg};
use sha2::Digest;
use tokio::fs;
use tokio::io::AsyncSeekExt;
use tokio::sync::Mutex;
use std::collections::HashMap;
use std::io::SeekFrom;
use std::path::Path;
use std::sync::Arc;
use serde::Deserialize;
const DESCRIPTION: &str = r#"
Transform a Cargo-based WebAssembly project to a bindle.
This reads a Cargo.toml file and the targets/ directory to construct a
Bindle from the data.
By default, it attempts to read the local Cargo.toml and targets/ directory, and then
write the results to a bindir/ directory as a standalone bindle.
"#;
#[tokio::main]
async fn main() {
let app = App::new("cargo2bindle")
.version(clap::crate_version!())
.author("DeisLabs at Microsoft Azure")
.about(DESCRIPTION)
.arg(
Arg::new("cargo")
.help("path to directory with cargo.toml")
.short('c')
.long("cargo")
.takes_value(true),
)
.arg(
Arg::new("bindir")
.help("path to bindle directory")
.short('b')
.long("bindle")
.takes_value(true),
)
.get_matches();
let cargo_dir = app.value_of("cargo").unwrap_or("./");
let bindle_dir = app.value_of("bindir").unwrap_or("./");
let cargo_toml = Path::new(cargo_dir).join("Cargo.toml");
let cargo_raw = fs::read(cargo_toml)
.await
.expect("Cargo.toml failed to load");
let cargo: Cargo = toml::from_slice(&cargo_raw).expect("Cargo.toml failed to parse");
let wasm_release_dir = Path::new(cargo_dir).join("target/wasm32-wasi/release");
if !wasm_release_dir.is_dir() {
panic!("not a directory");
}
let parcel_map = Arc::new(Mutex::new(HashMap::new()));
let mut stream = fs::read_dir(wasm_release_dir)
.await
.expect("unable to read wasm release directory");
let mut paths = Vec::new();
while let Some(entry) = stream.next_entry().await.expect("Unable to read directory") {
let path = entry.path();
let metadata = fs::metadata(&path)
.await
.expect("unable to check for file metadata");
if !metadata.is_dir()
&& path.extension().unwrap_or_else(|| std::ffi::OsStr::new("")) == "wasm"
{
paths.push(path)
}
}
let parcel_futures =
paths
.into_iter()
.map(|p| (p, parcel_map.clone()))
.map(|(path, parcel_map)| async move {
let mut file = tokio::fs::File::open(&path)
.await
.expect("file cannot be opened");
let mut hasher = bindle::async_util::AsyncSha256::new();
tokio::io::copy(&mut file, &mut hasher)
.await
.expect("hashing file failed");
let sha = format!("{:x}", hasher.into_inner().unwrap().finalize());
file.seek(SeekFrom::Start(0))
.await
.expect("failed to seek to beginning of WASM file");
parcel_map.lock().await.insert(sha.clone(), file);
let md = tokio::fs::metadata(&path).await.expect("failed to stat");
let label = bindle::Label {
name: format!("{}", path.file_name().unwrap().to_string_lossy()),
media_type: "application/wasm".to_owned(),
sha256: sha,
size: md.len(),
..bindle::Label::default()
};
bindle::Parcel {
label,
conditions: None,
}
});
let parcels: Vec<bindle::Parcel> = futures::future::join_all(parcel_futures).await;
let mut invoice = bindle::Invoice {
bindle_version: bindle::BINDLE_VERSION_1.to_owned(),
yanked: None,
yanked_signature: None,
bindle: bindle::BindleSpec {
id: format!("{}/{}", cargo.package.name, cargo.package.version)
.parse()
.expect("Missing name or version information"),
authors: cargo.package.authors,
description: cargo.package.description,
},
parcel: None,
annotations: None,
group: None,
signature: None,
};
if !parcels.is_empty() {
invoice.parcel = Some(parcels);
}
let standalone = bindle::standalone::StandaloneWrite::new(bindle_dir, &invoice.bindle.id)
.await
.expect("Invalid invoice");
standalone
.write(invoice, Arc::try_unwrap(parcel_map).unwrap().into_inner())
.await
.expect("unable to write data to standalone bindle");
println!("Wrote bindle to {}", standalone.path().display());
}
#[derive(Deserialize)]
struct Cargo {
package: Package,
}
#[derive(Deserialize)]
struct Package {
name: String,
version: String,
authors: Option<Vec<String>>,
description: Option<String>,
}