#![deny(missing_docs)]
#![allow(clippy::single_match, clippy::result_large_err)]
use std::io::Write;
use announce::{TagMode, TagSettings};
use axoasset::{LocalAsset, RemoteAsset};
use axoprocess::Cmd;
use backend::{
ci::CiInfo,
installer::{self, msi::MsiInstallerInfo, InstallerImpl},
};
use build::generic::{build_generic_target, run_extra_artifacts_build};
use build::{
cargo::{build_cargo_target, rustup_toolchain},
fake::{build_fake_cargo_target, build_fake_generic_target},
};
use camino::{Utf8Path, Utf8PathBuf};
use cargo_dist_schema::{ArtifactId, DistManifest};
use config::{
ArtifactMode, ChecksumStyle, CompressionImpl, Config, DirtyMode, GenerateMode, ZipStyle,
};
use console::Term;
use semver::Version;
use temp_dir::TempDir;
use tracing::info;
use errors::*;
pub use init::{do_init, InitArgs};
pub use tasks::*;
pub mod announce;
pub mod backend;
pub mod build;
pub mod config;
pub mod env;
pub mod errors;
pub mod host;
mod init;
pub mod linkage;
pub mod manifest;
pub mod platform;
pub mod sign;
pub mod tasks;
#[cfg(test)]
mod tests;
pub fn do_build(cfg: &Config) -> DistResult<DistManifest> {
check_integrity(cfg)?;
let (dist, mut manifest) = tasks::gather_work(cfg)?;
if !dist.dist_dir.exists() {
LocalAsset::create_dir_all(&dist.dist_dir)?;
}
eprintln!("building artifacts:");
for artifact in &dist.artifacts {
eprintln!(" {}", artifact.id);
init_artifact_dir(&dist, artifact)?;
}
eprintln!();
for step in &dist.local_build_steps {
if dist.local_builds_are_lies {
build_fake(&dist, step, &mut manifest)?;
} else {
run_build_step(&dist, step, &mut manifest)?;
}
}
for step in &dist.global_build_steps {
if dist.local_builds_are_lies {
build_fake(&dist, step, &mut manifest)?;
} else {
run_build_step(&dist, step, &mut manifest)?;
}
}
Ok(manifest)
}
pub fn do_manifest(cfg: &Config) -> DistResult<DistManifest> {
check_integrity(cfg)?;
let (_dist, manifest) = gather_work(cfg)?;
Ok(manifest)
}
fn run_build_step(
dist_graph: &DistGraph,
target: &BuildStep,
manifest: &mut DistManifest,
) -> DistResult<()> {
match target {
BuildStep::Generic(target) => build_generic_target(dist_graph, manifest, target)?,
BuildStep::Cargo(target) => build_cargo_target(dist_graph, manifest, target)?,
BuildStep::Rustup(cmd) => rustup_toolchain(dist_graph, cmd)?,
BuildStep::CopyFile(CopyStep {
src_path,
dest_path,
}) => copy_file(src_path, dest_path)?,
BuildStep::CopyDir(CopyStep {
src_path,
dest_path,
}) => copy_dir(src_path, dest_path)?,
BuildStep::CopyFileOrDir(CopyStep {
src_path,
dest_path,
}) => copy_file_or_dir(src_path, dest_path)?,
BuildStep::Zip(ZipDirStep {
src_path,
dest_path,
zip_style,
with_root,
}) => zip_dir(src_path, dest_path, zip_style, with_root.as_deref())?,
BuildStep::GenerateInstaller(installer) => {
generate_installer(dist_graph, installer, manifest)?
}
BuildStep::Checksum(ChecksumImpl {
checksum,
src_path,
dest_path,
for_artifact,
}) => generate_and_write_checksum(
manifest,
checksum,
src_path,
dest_path.as_deref(),
for_artifact.as_ref(),
)?,
BuildStep::GenerateSourceTarball(SourceTarballStep {
committish,
prefix,
target,
working_dir,
}) => generate_source_tarball(dist_graph, committish, prefix, target, working_dir)?,
BuildStep::Extra(target) => run_extra_artifacts_build(dist_graph, target)?,
BuildStep::Updater(updater) => fetch_updater(dist_graph, updater)?,
};
Ok(())
}
const AXOUPDATER_ASSET_ROOT: &str =
"https://github.com/axodotdev/axoupdater/releases/latest/download";
pub fn fetch_updater(dist_graph: &DistGraph, updater: &UpdaterStep) -> DistResult<()> {
let ext = if updater.target_triple.contains("pc-windows") {
".zip"
} else {
".tar.xz"
};
let expected_url = format!(
"{AXOUPDATER_ASSET_ROOT}/axoupdater-cli-{}{ext}",
updater.target_triple
);
let client = reqwest::blocking::Client::new();
let resp = client
.head(&expected_url)
.send()
.map_err(|_| DistError::AxoupdaterReleaseCheckFailed {})?;
if resp.status().is_success() {
fetch_updater_from_binary(dist_graph, updater, &expected_url)
} else if resp.status() == reqwest::StatusCode::NOT_FOUND {
fetch_updater_from_source(dist_graph, updater)
} else {
Err(DistError::AxoupdaterReleaseCheckFailed {})
}
}
pub fn fetch_updater_from_source(dist_graph: &DistGraph, updater: &UpdaterStep) -> DistResult<()> {
let (_tmp_dir, tmp_root) = create_tmp()?;
let mut cmd = Cmd::new(
"cargo",
format!("Fetch installer for {}", updater.target_triple),
);
cmd.arg("install")
.arg("axoupdater-cli")
.arg("--root")
.arg(&tmp_root)
.current_dir(&tmp_root)
.arg("--bin")
.arg("axoupdater");
cmd.run()?;
let mut source = tmp_root.join("bin").join("axoupdater");
if updater.target_triple.contains("windows") {
source.set_extension("exe");
}
LocalAsset::copy(source, dist_graph.target_dir.join(&updater.target_filename))?;
Ok(())
}
fn create_tmp() -> DistResult<(TempDir, Utf8PathBuf)> {
let tmp_dir = TempDir::new()?;
let tmp_root =
Utf8PathBuf::from_path_buf(tmp_dir.path().to_owned()).expect("tempdir isn't utf8!?");
Ok((tmp_dir, tmp_root))
}
fn fetch_updater_from_binary(
dist_graph: &DistGraph,
updater: &UpdaterStep,
asset_url: &str,
) -> DistResult<()> {
let (_tmp_dir, tmp_root) = create_tmp()?;
let zipball_target = tmp_root.join("archive");
let handle = tokio::runtime::Handle::current();
let asset = handle.block_on(RemoteAsset::load_bytes(asset_url))?;
std::fs::write(&zipball_target, asset)?;
let suffix = if updater.target_triple.contains("windows") {
".exe"
} else {
""
};
let requested_filename = format!("axoupdater{suffix}");
let bytes = if asset_url.ends_with(".tar.xz") {
LocalAsset::untar_xz_file(&zipball_target, &requested_filename)?
} else if asset_url.ends_with(".tar.gz") {
LocalAsset::untar_gz_file(&zipball_target, &requested_filename)?
} else if asset_url.ends_with(".zip") {
LocalAsset::unzip_file(&zipball_target, &requested_filename)?
} else {
let extension = Utf8PathBuf::from(asset_url)
.extension()
.unwrap_or("unable to determine")
.to_owned();
return Err(DistError::UnrecognizedCompression { extension });
};
let target = dist_graph.target_dir.join(&updater.target_filename);
std::fs::write(target, bytes)?;
Ok(())
}
fn build_fake(
dist_graph: &DistGraph,
target: &BuildStep,
manifest: &mut DistManifest,
) -> DistResult<()> {
match target {
BuildStep::Generic(target) => build_fake_generic_target(dist_graph, manifest, target)?,
BuildStep::Cargo(target) => build_fake_cargo_target(dist_graph, manifest, target)?,
BuildStep::Rustup(_) => {}
BuildStep::CopyFile(CopyStep {
src_path,
dest_path,
}) => copy_file(src_path, dest_path)?,
BuildStep::CopyDir(CopyStep {
src_path,
dest_path,
}) => copy_dir(src_path, dest_path)?,
BuildStep::CopyFileOrDir(CopyStep {
src_path,
dest_path,
}) => copy_file_or_dir(src_path, dest_path)?,
BuildStep::Zip(ZipDirStep {
src_path,
dest_path,
zip_style,
with_root,
}) => zip_dir(src_path, dest_path, zip_style, with_root.as_deref())?,
BuildStep::GenerateInstaller(installer) => match installer {
InstallerImpl::Msi(msi) => generate_fake_msi(dist_graph, msi, manifest)?,
_ => generate_installer(dist_graph, installer, manifest)?,
},
BuildStep::Checksum(ChecksumImpl {
checksum,
src_path,
dest_path,
for_artifact,
}) => generate_and_write_checksum(
manifest,
checksum,
src_path,
dest_path.as_deref(),
for_artifact.as_ref(),
)?,
BuildStep::GenerateSourceTarball(SourceTarballStep {
committish,
prefix,
target,
working_dir,
}) => generate_fake_source_tarball(dist_graph, committish, prefix, target, working_dir)?,
BuildStep::Extra(target) => run_fake_extra_artifacts_build(dist_graph, target)?,
BuildStep::Updater(_) => unimplemented!(),
}
Ok(())
}
fn run_fake_extra_artifacts_build(dist: &DistGraph, target: &ExtraBuildStep) -> DistResult<()> {
for artifact in &target.artifact_relpaths {
let path = dist.dist_dir.join(artifact);
LocalAsset::write_new_all("", &path)?;
}
Ok(())
}
fn generate_fake_msi(
_dist: &DistGraph,
msi: &MsiInstallerInfo,
_manifest: &DistManifest,
) -> DistResult<()> {
LocalAsset::write_new_all("", &msi.file_path)?;
Ok(())
}
fn generate_and_write_checksum(
manifest: &mut DistManifest,
checksum: &ChecksumStyle,
src_path: &Utf8Path,
dest_path: Option<&Utf8Path>,
for_artifact: Option<&ArtifactId>,
) -> DistResult<()> {
let output = generate_checksum(checksum, src_path)?;
if let Some(dest_path) = dest_path {
write_checksum(&output, src_path, dest_path)?;
}
if let Some(artifact_id) = for_artifact {
if let Some(artifact) = manifest.artifacts.get_mut(artifact_id) {
artifact.checksums.insert(checksum.ext().to_owned(), output);
}
}
Ok(())
}
fn generate_checksum(checksum: &ChecksumStyle, src_path: &Utf8Path) -> DistResult<String> {
info!("generating {checksum:?} for {src_path}");
use sha2::Digest;
use std::fmt::Write;
let file_bytes = axoasset::LocalAsset::load_bytes(src_path.as_str())?;
let hash = match checksum {
ChecksumStyle::Sha256 => {
let mut hasher = sha2::Sha256::new();
hasher.update(&file_bytes);
hasher.finalize().as_slice().to_owned()
}
ChecksumStyle::Sha512 => {
let mut hasher = sha2::Sha512::new();
hasher.update(&file_bytes);
hasher.finalize().as_slice().to_owned()
}
ChecksumStyle::Sha3_256 => {
let mut hasher = sha3::Sha3_256::new();
hasher.update(&file_bytes);
hasher.finalize().as_slice().to_owned()
}
ChecksumStyle::Sha3_512 => {
let mut hasher = sha3::Sha3_512::new();
hasher.update(&file_bytes);
hasher.finalize().as_slice().to_owned()
}
ChecksumStyle::Blake2s => {
let mut hasher = blake2::Blake2s256::new();
hasher.update(&file_bytes);
hasher.finalize().as_slice().to_owned()
}
ChecksumStyle::Blake2b => {
let mut hasher = blake2::Blake2b512::new();
hasher.update(&file_bytes);
hasher.finalize().as_slice().to_owned()
}
ChecksumStyle::False => {
unreachable!()
}
};
let mut output = String::new();
for byte in hash {
write!(&mut output, "{:02x}", byte).unwrap();
}
Ok(output)
}
fn generate_source_tarball(
graph: &DistGraph,
committish: &str,
prefix: &str,
target: &Utf8Path,
working_dir: &Utf8Path,
) -> DistResult<()> {
let git = if let Some(tool) = &graph.tools.git {
tool.cmd.to_owned()
} else {
return Err(DistError::ToolMissing {
tool: "git".to_owned(),
});
};
Cmd::new(git, "generate a source tarball for your project")
.arg("archive")
.arg(committish)
.arg("--format=tar.gz")
.arg("--prefix")
.arg(prefix)
.arg("--output")
.arg(target)
.current_dir(working_dir)
.run()?;
Ok(())
}
fn generate_fake_source_tarball(
_graph: &DistGraph,
_committish: &str,
_prefix: &str,
target: &Utf8Path,
_working_dir: &Utf8Path,
) -> DistResult<()> {
LocalAsset::write_new_all("", target)?;
Ok(())
}
fn write_checksum(checksum: &str, src_path: &Utf8Path, dest_path: &Utf8Path) -> DistResult<()> {
let file_path = src_path.file_name().expect("hashing file with no name!?");
let line = format!("{checksum} *{file_path}\n");
axoasset::LocalAsset::write_new(&line, dest_path)?;
Ok(())
}
fn init_artifact_dir(_dist: &DistGraph, artifact: &Artifact) -> DistResult<()> {
if artifact.file_path.exists() {
LocalAsset::remove_file(&artifact.file_path)?;
}
let Some(archive) = &artifact.archive else {
return Ok(());
};
info!("recreating artifact dir: {}", archive.dir_path);
if archive.dir_path.exists() {
LocalAsset::remove_dir_all(&archive.dir_path)?;
}
LocalAsset::create_dir(&archive.dir_path)?;
Ok(())
}
pub(crate) fn copy_file(src_path: &Utf8Path, dest_path: &Utf8Path) -> DistResult<()> {
LocalAsset::copy_named(src_path, dest_path)?;
Ok(())
}
pub(crate) fn copy_dir(src_path: &Utf8Path, dest_path: &Utf8Path) -> DistResult<()> {
LocalAsset::copy_dir_named(src_path, dest_path)?;
Ok(())
}
pub(crate) fn copy_file_or_dir(src_path: &Utf8Path, dest_path: &Utf8Path) -> DistResult<()> {
if src_path.is_dir() {
copy_dir(src_path, dest_path)
} else {
copy_file(src_path, dest_path)
}
}
fn zip_dir(
src_path: &Utf8Path,
dest_path: &Utf8Path,
zip_style: &ZipStyle,
with_root: Option<&Utf8Path>,
) -> DistResult<()> {
match zip_style {
ZipStyle::Zip => LocalAsset::zip_dir(src_path, dest_path, with_root)?,
ZipStyle::Tar(CompressionImpl::Gzip) => {
LocalAsset::tar_gz_dir(src_path, dest_path, with_root)?
}
ZipStyle::Tar(CompressionImpl::Xzip) => {
LocalAsset::tar_xz_dir(src_path, dest_path, with_root)?
}
ZipStyle::Tar(CompressionImpl::Zstd) => {
LocalAsset::tar_zstd_dir(src_path, dest_path, with_root)?
}
ZipStyle::TempDir => {
}
}
Ok(())
}
#[derive(Debug)]
pub struct GenerateArgs {
pub check: bool,
pub modes: Vec<GenerateMode>,
}
fn do_generate_preflight_checks(dist: &DistGraph) -> DistResult<()> {
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-")
&& !matches!(dist.allow_dirty, DirtyMode::AllowAll)
{
return Err(DistError::MismatchedDistVersion {
config_version: desired_version.to_string(),
running_version: current_version.to_string(),
});
}
}
if !dist.is_init {
return Err(DistError::NeedsInit);
}
Ok(())
}
pub fn do_generate(cfg: &Config, args: &GenerateArgs) -> DistResult<()> {
let (dist, _manifest) = gather_work(cfg)?;
run_generate(&dist, args)?;
Ok(())
}
pub fn run_generate(dist: &DistGraph, args: &GenerateArgs) -> DistResult<()> {
do_generate_preflight_checks(dist)?;
let inferred = args.modes.is_empty();
let modes = if inferred {
&[GenerateMode::Ci, GenerateMode::Msi]
} else {
for &mode in &args.modes {
if !dist.allow_dirty.should_run(mode)
&& matches!(dist.allow_dirty, DirtyMode::AllowList(..))
{
Err(DistError::ContradictoryGenerateModes {
generate_mode: mode,
})?;
}
}
&args.modes[..]
};
for &mode in modes {
if dist.allow_dirty.should_run(mode) {
match mode {
GenerateMode::Ci => {
let CiInfo { github } = &dist.ci;
if let Some(github) = github {
if args.check {
github.check(dist)?;
} else {
github.write_to_disk(dist)?;
}
}
}
GenerateMode::Msi => {
for artifact in &dist.artifacts {
if let ArtifactKind::Installer(InstallerImpl::Msi(msi)) = &artifact.kind {
if args.check {
msi.check_config()?;
} else {
msi.write_config_to_disk()?;
}
}
}
}
}
}
}
Ok(())
}
pub fn check_integrity(cfg: &Config) -> DistResult<()> {
let check_config = Config {
tag_settings: TagSettings {
needs_coherence: false,
tag: TagMode::Infer,
},
create_hosting: false,
artifact_mode: ArtifactMode::All,
no_local_paths: false,
allow_all_dirty: cfg.allow_all_dirty,
targets: vec![],
ci: vec![],
installers: vec![],
root_cmd: "check".to_owned(),
};
let (dist, _manifest) = tasks::gather_work(&check_config)?;
if let Some(hosting) = &dist.hosting {
if hosting.hosts.contains(&config::HostingStyle::Axodotdev) {
let mut out = Term::stderr();
let info = "INFO:";
let message = r"You've enabled Axo Releases, which is currently in Closed Beta.
If you haven't yet signed up, please join our discord
(https://discord.gg/ECnWuUUXQk) or message hello@axo.dev to get started!
";
writeln!(out, "{} {}", out.style().yellow().apply_to(info), message).unwrap();
}
}
run_generate(
&dist,
&GenerateArgs {
modes: vec![],
check: true,
},
)
}
fn generate_installer(
dist: &DistGraph,
style: &InstallerImpl,
manifest: &DistManifest,
) -> DistResult<()> {
match style {
InstallerImpl::Shell(info) => installer::shell::write_install_sh_script(dist, info)?,
InstallerImpl::Powershell(info) => {
installer::powershell::write_install_ps_script(dist, info)?
}
InstallerImpl::Npm(info) => installer::npm::write_npm_project(dist, info)?,
InstallerImpl::Homebrew(info) => {
installer::homebrew::write_homebrew_formula(dist, info, manifest)?
}
InstallerImpl::Msi(info) => info.build(dist)?,
}
Ok(())
}
pub fn default_desktop_targets() -> Vec<String> {
vec![
axoproject::platforms::TARGET_X64_LINUX_GNU.to_owned(),
axoproject::platforms::TARGET_X64_WINDOWS.to_owned(),
axoproject::platforms::TARGET_X64_MAC.to_owned(),
axoproject::platforms::TARGET_ARM64_MAC.to_owned(),
]
}
pub fn known_desktop_targets() -> Vec<String> {
vec![
axoproject::platforms::TARGET_X64_LINUX_GNU.to_owned(),
axoproject::platforms::TARGET_X64_LINUX_MUSL.to_owned(),
axoproject::platforms::TARGET_X64_WINDOWS.to_owned(),
axoproject::platforms::TARGET_X64_MAC.to_owned(),
axoproject::platforms::TARGET_ARM64_MAC.to_owned(),
]
}