#![deny(missing_docs)]
#![allow(clippy::single_match)]
use std::{
collections::{BTreeMap, HashMap},
fs::File,
io::BufReader,
process::Command,
};
use camino::{Utf8Path, Utf8PathBuf};
use cargo_dist_schema::{Asset, AssetKind, DistManifest, ExecutableAsset, Release};
use flate2::{write::ZlibEncoder, Compression, GzBuilder};
use semver::Version;
use tracing::{info, warn};
use xz2::write::XzEncoder;
use zip::ZipWriter;
use errors::*;
pub use init::{do_init, InitArgs};
use miette::{miette, Context, IntoDiagnostic};
pub use tasks::*;
pub mod ci;
pub mod errors;
mod init;
pub mod installer;
pub mod tasks;
#[cfg(test)]
mod tests;
pub fn do_dist(cfg: &Config) -> Result<DistManifest> {
let dist = tasks::gather_work(cfg)?;
if !dist.is_init {
return Err(miette!(
"please run 'cargo dist init' before running any other commands!"
));
}
if !dist.dist_dir.exists() {
std::fs::create_dir_all(&dist.dist_dir)
.into_diagnostic()
.wrap_err_with(|| format!("couldn't create dist target dir at {}", dist.dist_dir))?;
}
for artifact in &dist.artifacts {
eprintln!("bundling {}", artifact.id);
init_artifact_dir(&dist, artifact)?;
}
for step in &dist.build_steps {
run_build_step(&dist, step)?;
}
for artifact in &dist.artifacts {
eprintln!("bundled: {}", artifact.file_path);
}
Ok(build_manifest(cfg, &dist))
}
pub fn do_manifest(cfg: &Config) -> Result<DistManifest> {
let dist = gather_work(cfg)?;
if !dist.is_init {
return Err(miette!(
"please run 'cargo dist init' before running any other commands!"
));
}
Ok(build_manifest(cfg, &dist))
}
fn build_manifest(cfg: &Config, dist: &DistGraph) -> DistManifest {
let mut releases = vec![];
let mut all_artifacts = BTreeMap::<String, cargo_dist_schema::Artifact>::new();
for release in &dist.releases {
let mut artifacts = vec![];
for &artifact_idx in &release.global_artifacts {
let id = &dist.artifact(artifact_idx).id;
all_artifacts.insert(id.clone(), manifest_artifact(cfg, dist, artifact_idx));
artifacts.push(id.clone());
}
for &variant_idx in &release.variants {
let variant = dist.variant(variant_idx);
for &artifact_idx in &variant.local_artifacts {
let id = &dist.artifact(artifact_idx).id;
all_artifacts.insert(id.clone(), manifest_artifact(cfg, dist, artifact_idx));
artifacts.push(id.clone());
}
}
releases.push(Release {
app_name: release.app_name.clone(),
app_version: release.version.to_string(),
artifacts,
})
}
let mut manifest = DistManifest::new(releases, all_artifacts);
manifest.dist_version = Some(env!("CARGO_PKG_VERSION").to_owned());
manifest.announcement_tag = dist.announcement_tag.clone();
manifest.announcement_is_prerelease = dist.announcement_is_prerelease;
manifest.announcement_title = dist.announcement_title.clone();
manifest.announcement_changelog = dist.announcement_changelog.clone();
manifest.announcement_github_body = dist.announcement_github_body.clone();
manifest
}
fn manifest_artifact(
cfg: &Config,
dist: &DistGraph,
artifact_idx: ArtifactIdx,
) -> cargo_dist_schema::Artifact {
let artifact = dist.artifact(artifact_idx);
let mut assets = vec![];
let built_assets = artifact
.required_binaries
.iter()
.map(|(&binary_idx, exe_path)| {
let binary = &dist.binary(binary_idx);
let symbols_artifact = binary.symbols_artifact.map(|a| dist.artifact(a).id.clone());
Asset {
name: Some(binary.name.clone()),
path: Some(exe_path.file_name().unwrap().to_owned()),
kind: AssetKind::Executable(ExecutableAsset { symbols_artifact }),
}
});
let mut static_assets = artifact
.archive
.as_ref()
.map(|archive| {
archive
.static_assets
.iter()
.map(|(kind, asset)| {
let kind = match kind {
StaticAssetKind::Changelog => AssetKind::Changelog,
StaticAssetKind::License => AssetKind::License,
StaticAssetKind::Readme => AssetKind::Readme,
StaticAssetKind::Other => AssetKind::Unknown,
};
Asset {
name: Some(asset.file_name().unwrap().to_owned()),
path: Some(asset.file_name().unwrap().to_owned()),
kind,
}
})
.collect::<Vec<_>>()
})
.unwrap_or_default();
if let ArtifactKind::Installer(InstallerImpl::Npm(..)) = &artifact.kind {
for &asset in installer::NPM_PACKAGE_CONTENTS {
static_assets.push(Asset {
name: Some(asset.to_owned()),
path: Some(asset.to_owned()),
kind: AssetKind::Unknown,
});
}
}
assets.extend(built_assets);
assets.extend(static_assets);
assets.sort_by(|k1, k2| k1.name.cmp(&k2.name));
let install_hint;
let description;
let kind;
match &artifact.kind {
ArtifactKind::ExecutableZip(_) => {
install_hint = None;
description = None;
kind = cargo_dist_schema::ArtifactKind::ExecutableZip;
}
ArtifactKind::Symbols(_) => {
install_hint = None;
description = None;
kind = cargo_dist_schema::ArtifactKind::Symbols;
}
ArtifactKind::Installer(
InstallerImpl::Powershell(info)
| InstallerImpl::Shell(info)
| InstallerImpl::Npm(NpmInstallerInfo { inner: info, .. }),
) => {
install_hint = Some(info.hint.clone());
description = Some(info.desc.clone());
kind = cargo_dist_schema::ArtifactKind::Installer;
}
};
cargo_dist_schema::Artifact {
name: Some(artifact.id.clone()),
path: if cfg.no_local_paths {
None
} else {
Some(artifact.file_path.to_string())
},
target_triples: artifact.target_triples.clone(),
install_hint,
description,
assets,
kind,
}
}
fn run_build_step(dist_graph: &DistGraph, target: &BuildStep) -> Result<()> {
match target {
BuildStep::Cargo(target) => build_cargo_target(dist_graph, target),
BuildStep::Rustup(cmd) => rustup_toolchain(dist_graph, cmd),
BuildStep::CopyFile(CopyFileStep {
src_path,
dest_path,
}) => copy_file(src_path, dest_path),
BuildStep::CopyDir(CopyDirStep {
src_path,
dest_path,
}) => copy_dir(src_path, dest_path),
BuildStep::Zip(ZipDirStep {
src_path,
dest_path,
zip_style,
dir_name,
}) => zip_dir(src_path, dest_path, zip_style, dir_name),
BuildStep::GenerateInstaller(installer) => generate_installer(dist_graph, installer),
}
}
fn build_cargo_target(dist_graph: &DistGraph, target: &CargoBuildStep) -> Result<()> {
eprintln!(
"building cargo target ({}/{})",
target.target_triple, target.profile
);
let mut command = Command::new(&dist_graph.cargo);
command
.arg("build")
.arg("--profile")
.arg(&target.profile)
.arg("--message-format=json")
.arg("--target")
.arg(&target.target_triple)
.env("RUSTFLAGS", &target.rustflags)
.stdout(std::process::Stdio::piped());
if target.features.no_default_features {
command.arg("--no-default-features");
}
match &target.features.features {
CargoTargetFeatureList::All => {
command.arg("--all-features");
}
CargoTargetFeatureList::List(features) => {
if !features.is_empty() {
command.arg("--features");
for feature in features {
command.arg(feature);
}
}
}
}
match &target.package {
CargoTargetPackages::Workspace => {
command.arg("--workspace");
}
CargoTargetPackages::Package(package) => {
command.arg("--package").arg(package.to_string());
}
}
info!("exec: {:?}", command);
let mut task = command
.spawn()
.into_diagnostic()
.wrap_err_with(|| format!("failed to exec cargo build: {command:?}"))?;
let mut expected_exes = HashMap::<String, HashMap<String, (Utf8PathBuf, Utf8PathBuf)>>::new();
let mut expected_symbols =
HashMap::<String, HashMap<String, (Utf8PathBuf, Utf8PathBuf)>>::new();
for &binary_idx in &target.expected_binaries {
let binary = &dist_graph.binary(binary_idx);
let package_id = binary.pkg_id.to_string();
let exe_name = binary.name.clone();
for exe_dest in &binary.copy_exe_to {
expected_exes
.entry(package_id.clone())
.or_default()
.insert(exe_name.clone(), (Utf8PathBuf::new(), exe_dest.clone()));
}
for sym_dest in &binary.copy_symbols_to {
expected_symbols
.entry(package_id.clone())
.or_default()
.insert(exe_name.clone(), (Utf8PathBuf::new(), sym_dest.clone()));
}
}
let reader = std::io::BufReader::new(task.stdout.take().unwrap());
for message in cargo_metadata::Message::parse_stream(reader) {
let Ok(message) = message.into_diagnostic().wrap_err("failed to parse cargo json message").map_err(|e| warn!("{:?}", e)) else {
continue;
};
match message {
cargo_metadata::Message::CompilerArtifact(artifact) => {
if let Some(new_exe) = artifact.executable {
info!("got a new exe: {}", new_exe);
let package_id = artifact.package_id.to_string();
let exe_name = new_exe.file_stem().unwrap();
let expected_sym = expected_symbols
.get_mut(&package_id)
.and_then(|m| m.get_mut(exe_name));
if let Some((src_sym_path, _)) = expected_sym {
for path in artifact.filenames {
let is_symbols = path.extension().map(|e| e == "pdb").unwrap_or(false);
if is_symbols {
*src_sym_path = path;
}
}
}
let expected_exe = expected_exes
.get_mut(&package_id)
.and_then(|m| m.get_mut(exe_name));
if let Some(expected) = expected_exe {
expected.0 = new_exe;
}
}
}
_ => {
}
}
}
for (package_id, exes) in expected_exes {
for (exe_name, (src_path, dest_path)) in &exes {
if src_path.as_str().is_empty() {
return Err(miette!("failed to find bin {} ({})", exe_name, package_id));
}
copy_file(src_path, dest_path)?;
}
}
for (package_id, symbols) in expected_symbols {
for (exe, (src_path, dest_path)) in &symbols {
if src_path.as_str().is_empty() {
return Err(miette!(
"failed to find symbols for bin {} ({})",
exe,
package_id
));
}
copy_file(src_path, dest_path)?;
}
}
Ok(())
}
fn rustup_toolchain(_dist_graph: &DistGraph, cmd: &RustupStep) -> Result<()> {
eprintln!("running rustup to ensure you have {} installed", cmd.target);
let status = Command::new(&cmd.rustup.cmd)
.arg("target")
.arg("add")
.arg(&cmd.target)
.status()
.into_diagnostic()
.wrap_err("Failed to install rustup toolchain")?;
if !status.success() {
return Err(miette!("Failed to install rustup toolchain"));
}
Ok(())
}
fn init_artifact_dir(_dist: &DistGraph, artifact: &Artifact) -> Result<()> {
if artifact.file_path.exists() {
std::fs::remove_file(&artifact.file_path)
.into_diagnostic()
.wrap_err_with(|| format!("failed to delete old artifact {}", artifact.file_path))?;
}
let Some(archive) = &artifact.archive else {
return Ok(());
};
info!("recreating artifact dir: {}", archive.dir_path);
if archive.dir_path.exists() {
std::fs::remove_dir_all(&archive.dir_path)
.into_diagnostic()
.wrap_err_with(|| format!("failed to delete old artifact dir {}", archive.dir_path))?;
}
std::fs::create_dir(&archive.dir_path)
.into_diagnostic()
.wrap_err_with(|| format!("failed to create artifact dir {}", archive.dir_path))?;
Ok(())
}
pub(crate) fn copy_file(src_path: &Utf8Path, dest_path: &Utf8Path) -> Result<()> {
let _bytes_written = std::fs::copy(src_path, dest_path)
.into_diagnostic()
.wrap_err_with(|| format!("failed to copy file {src_path} => {dest_path}"))?;
Ok(())
}
fn copy_dir(_src_path: &Utf8Path, _dest_path: &Utf8Path) -> Result<()> {
todo!("copy_dir isn't implemented yet")
}
fn zip_dir(
src_path: &Utf8Path,
dest_path: &Utf8Path,
zip_style: &ZipStyle,
dir_name: &str,
) -> Result<()> {
match zip_style {
ZipStyle::Zip => really_zip_dir(src_path, dest_path, dir_name),
ZipStyle::Tar(compression) => tar_dir(src_path, dest_path, compression, dir_name),
}
}
fn tar_dir(
src_path: &Utf8Path,
dest_path: &Utf8Path,
compression: &CompressionImpl,
dir_name: &str,
) -> Result<()> {
let zip_contents_name = format!("{dir_name}.tar");
let final_zip_file = File::create(dest_path)
.into_diagnostic()
.wrap_err_with(|| format!("failed to create file for artifact: {dest_path}"))?;
match compression {
CompressionImpl::Gzip => {
let zip_output = GzBuilder::new()
.filename(zip_contents_name)
.write(final_zip_file, Compression::default());
let mut tar = tar::Builder::new(zip_output);
tar.append_dir_all(dir_name, src_path)
.into_diagnostic()
.wrap_err_with(|| {
format!("failed to copy directory into tar: {src_path} => {dir_name}",)
})?;
let zip_output = tar
.into_inner()
.into_diagnostic()
.wrap_err_with(|| format!("failed to write tar: {dest_path}"))?;
let _zip_file = zip_output
.finish()
.into_diagnostic()
.wrap_err_with(|| format!("failed to write archive: {dest_path}"))?;
}
CompressionImpl::Xzip => {
let zip_output = XzEncoder::new(final_zip_file, 9);
let mut tar = tar::Builder::new(zip_output);
tar.append_dir_all(dir_name, src_path)
.into_diagnostic()
.wrap_err_with(|| {
format!("failed to copy directory into tar: {src_path} => {dir_name}",)
})?;
let zip_output = tar
.into_inner()
.into_diagnostic()
.wrap_err_with(|| format!("failed to write tar: {dest_path}"))?;
let _zip_file = zip_output
.finish()
.into_diagnostic()
.wrap_err_with(|| format!("failed to write archive: {dest_path}"))?;
}
CompressionImpl::Zstd => {
let zip_output = ZlibEncoder::new(final_zip_file, Compression::default());
let mut tar = tar::Builder::new(zip_output);
tar.append_dir_all(dir_name, src_path)
.into_diagnostic()
.wrap_err_with(|| {
format!("failed to copy directory into tar: {src_path} => {dir_name}",)
})?;
let zip_output = tar
.into_inner()
.into_diagnostic()
.wrap_err_with(|| format!("failed to write tar: {dest_path}"))?;
let _zip_file = zip_output
.finish()
.into_diagnostic()
.wrap_err_with(|| format!("failed to write archive: {dest_path}"))?;
}
}
info!("artifact created at: {}", dest_path);
Ok(())
}
fn really_zip_dir(src_path: &Utf8Path, dest_path: &Utf8Path, _dir_name: &str) -> Result<()> {
let final_zip_file = File::create(dest_path)
.into_diagnostic()
.wrap_err_with(|| format!("failed to create file for artifact: {dest_path}"))?;
let mut zip = ZipWriter::new(final_zip_file);
let dir = std::fs::read_dir(src_path)
.into_diagnostic()
.wrap_err_with(|| format!("failed to read artifact dir: {src_path}"))?;
for entry in dir {
let entry = entry.into_diagnostic()?;
if entry.file_type().into_diagnostic()?.is_file() {
let options = zip::write::FileOptions::default()
.compression_method(zip::CompressionMethod::Stored);
let file = File::open(entry.path()).into_diagnostic()?;
let mut buf = BufReader::new(file);
let file_name = entry.file_name();
let utf8_file_name = file_name.to_string_lossy();
zip.start_file(utf8_file_name.clone(), options)
.into_diagnostic()
.wrap_err_with(|| {
format!("failed to create file {utf8_file_name} in zip: {dest_path}")
})?;
std::io::copy(&mut buf, &mut zip).into_diagnostic()?;
} else {
todo!("implement zip subdirs! (or was this a symlink?)");
}
}
let _zip_file = zip
.finish()
.into_diagnostic()
.wrap_err_with(|| format!("failed to write archive: {dest_path}"))?;
info!("artifact created at: {}", dest_path);
Ok(())
}
#[derive(Debug)]
pub struct GenerateCiArgs {}
pub fn do_generate_ci(cfg: &Config, _args: &GenerateCiArgs) -> Result<()> {
let dist = gather_work(cfg)?;
if let Some(desired_version) = &dist.desired_cargo_dist_version {
let current_version: Version = std::env!("CARGO_PKG_VERSION").parse().unwrap();
if desired_version != ¤t_version && !desired_version.pre.starts_with("github-") {
return Err(miette!("you're running cargo-dist {}, but 'cargo-dist-version = {}' is set in your Cargo.toml\n\nYou should update cargo-dist-version if you want to update to this version", current_version, desired_version));
}
}
if !dist.is_init {
return Err(miette!(
"please run 'cargo dist init' before running any other commands!"
));
}
for style in &dist.ci_style {
match style {
CiStyle::Github => ci::generate_github_ci(&dist)?,
}
}
Ok(())
}
fn generate_installer(dist: &DistGraph, style: &InstallerImpl) -> Result<()> {
match style {
InstallerImpl::Shell(info) => installer::generate_install_sh_script(dist, info),
InstallerImpl::Powershell(info) => installer::generate_install_ps_script(dist, info),
InstallerImpl::Npm(info) => installer::generate_install_npm_project(dist, info),
}
}