cargo_dist/
lib.rs

1#![deny(missing_docs)]
2#![allow(clippy::single_match, clippy::result_large_err)]
3
4//! # dist
5//!
6//! This is the library at the core of the 'dist' CLI. It currently mostly exists
7//! for the sake of internal documentation/testing, and isn't intended to be used by anyone else.
8//! That said, if you have a reason to use it, let us know!
9//!
10//! It's currently not terribly well-suited to being used as a pure library because it happily
11//! writes to stderr/stdout whenever it pleases. Suboptimal for a library.
12
13use std::io::Write;
14
15use announce::TagSettings;
16use axoasset::LocalAsset;
17use axoprocess::Cmd;
18use backend::{
19    ci::CiInfo,
20    installer::{
21        self, macpkg::PkgInstallerInfo, msi::MsiInstallerInfo, HomebrewImpl, InstallerImpl,
22    },
23};
24use build::generic::{build_generic_target, run_extra_artifacts_build};
25use build::{
26    cargo::{build_cargo_target, rustup_toolchain},
27    fake::{build_fake_cargo_target, build_fake_generic_target},
28};
29use camino::{Utf8Path, Utf8PathBuf};
30use cargo_dist_schema::{ArtifactId, ChecksumValue, ChecksumValueRef, DistManifest, TripleName};
31use config::{
32    ArtifactMode, ChecksumStyle, CompressionImpl, Config, DirtyMode, GenerateMode, ZipStyle,
33};
34use console::Term;
35use semver::Version;
36use temp_dir::TempDir;
37use tracing::info;
38
39use errors::*;
40pub use init::{do_init, do_migrate, InitArgs};
41pub use tasks::*;
42
43pub mod announce;
44pub mod backend;
45pub mod build;
46pub mod config;
47pub mod env;
48pub mod errors;
49pub mod host;
50mod init;
51pub mod linkage;
52pub mod manifest;
53pub mod net;
54pub mod platform;
55pub mod sign;
56pub mod tasks;
57#[cfg(test)]
58mod tests;
59
60/// dist env test -- make sure we have everything we need for a build.
61pub fn do_env_test(cfg: &Config) -> DistResult<()> {
62    let (dist, _manifest) = tasks::gather_work(cfg)?;
63
64    let local_builds = matches!(
65        cfg.artifact_mode,
66        ArtifactMode::Local | ArtifactMode::All | ArtifactMode::Host
67    );
68
69    let builds = dist.config.builds;
70
71    // cargo-auditable is used only in local builds
72    let need_cargo_auditable = builds.cargo.cargo_auditable && local_builds;
73    // omnibor is used in both local and global builds
74    let need_omnibor = builds.omnibor;
75    let mut need_xwin = false;
76    let mut need_zigbuild = false;
77
78    let tools = dist.tools;
79    let host = tools.host_target.parse()?;
80
81    for step in dist.local_build_steps.iter() {
82        // Can't require cross-compilation tools if we aren't compiling.
83        if cfg.artifact_mode == ArtifactMode::Lies {
84            break;
85        }
86
87        match step {
88            BuildStep::Cargo(step) => {
89                let target = step.target_triple.parse()?;
90                let wrapper = tasks::build_wrapper_for_cross(&host, &target)?;
91
92                match wrapper {
93                    Some(CargoBuildWrapper::Xwin) => {
94                        need_xwin = true;
95                    }
96                    Some(CargoBuildWrapper::ZigBuild) => {
97                        need_zigbuild = true;
98                    }
99                    None => {}
100                }
101            }
102            _ => {}
103        }
104    }
105
106    // These are all of the tools we can check for.
107    //
108    // bool::then(f) returns an Option, so we start with a
109    // Vec<Option<Result<&Tool, DistResult>>>.
110    let all_tools: Vec<Option<DistResult<&Tool>>> = vec![
111        need_cargo_auditable.then(|| tools.cargo_auditable()),
112        need_omnibor.then(|| tools.omnibor()),
113        need_xwin.then(|| tools.cargo_xwin()),
114        need_zigbuild.then(|| tools.cargo_zigbuild()),
115    ];
116
117    // Drop `None`s, then extract the values from the remaining `Option`s.
118    let needed_tools = all_tools.into_iter().flatten();
119
120    let missing: Vec<String> = needed_tools
121        .filter_map(|t| match t {
122            // The tool was found.
123            Ok(_) => None,
124            // The tool is missing
125            Err(DistError::ToolMissing { tool: ref name }) => Some(name.to_owned()),
126            // This should never happen, but I can't find a way to enforce
127            // it at the type system level. ;~; -@duckinator
128            Err(_) => unreachable!(
129                "do_env_test() got an Err that wasn't DistError::ToolMissing. This is a dist bug."
130            ),
131        })
132        .collect();
133
134    missing
135        .is_empty()
136        .then_some(())
137        .ok_or(DistError::EnvToolsMissing { tools: missing })
138}
139
140/// dist build -- actually build binaries and installers!
141pub fn do_build(cfg: &Config) -> DistResult<DistManifest> {
142    do_env_test(cfg)?;
143    check_integrity(cfg)?;
144
145    let (dist, mut manifest) = tasks::gather_work(cfg)?;
146
147    // FIXME: parallelize this by working this like a dependency graph, so we can start
148    // bundling up an executable the moment it's built! Note however that you shouldn't
149    // parallelize Cargo invocations because it has global state that can get clobbered.
150    // Most problematically if you do two builds with different feature flags the final
151    // binaries will get copied to the same location and clobber each other :(
152
153    // First set up our target dirs so things don't have to race to do it later
154    if !dist.dist_dir.exists() {
155        LocalAsset::create_dir_all(&dist.dist_dir)?;
156    }
157
158    eprintln!("building artifacts:");
159    for artifact in &dist.artifacts {
160        eprintln!("  {}", artifact.id);
161        init_artifact_dir(&dist, artifact)?;
162    }
163    eprintln!();
164
165    // Run all the local build steps first
166    for step in &dist.local_build_steps {
167        if dist.local_builds_are_lies {
168            build_fake(&dist, step, &mut manifest)?;
169        } else {
170            run_build_step(&dist, step, &mut manifest)?;
171        }
172    }
173
174    // Next the global steps
175    for step in &dist.global_build_steps {
176        if dist.local_builds_are_lies {
177            build_fake(&dist, step, &mut manifest)?;
178        } else {
179            run_build_step(&dist, step, &mut manifest)?;
180        }
181    }
182
183    Ok(manifest)
184}
185
186/// Just generate the manifest produced by `dist build` without building
187pub fn do_manifest(cfg: &Config) -> DistResult<DistManifest> {
188    check_integrity(cfg)?;
189    let (_dist, manifest) = gather_work(cfg)?;
190
191    Ok(manifest)
192}
193
194/// Run some build step
195fn run_build_step(
196    dist_graph: &DistGraph,
197    target: &BuildStep,
198    manifest: &mut DistManifest,
199) -> DistResult<()> {
200    match target {
201        BuildStep::Generic(target) => build_generic_target(dist_graph, manifest, target)?,
202        BuildStep::Cargo(target) => build_cargo_target(dist_graph, manifest, target)?,
203        BuildStep::Rustup(cmd) => rustup_toolchain(dist_graph, cmd)?,
204        BuildStep::CopyFile(CopyStep {
205            src_path,
206            dest_path,
207        }) => copy_file(src_path, dest_path)?,
208        BuildStep::CopyDir(CopyStep {
209            src_path,
210            dest_path,
211        }) => copy_dir(src_path, dest_path)?,
212        BuildStep::CopyFileOrDir(CopyStep {
213            src_path,
214            dest_path,
215        }) => copy_file_or_dir(src_path, dest_path)?,
216        BuildStep::Zip(ZipDirStep {
217            src_path,
218            dest_path,
219            zip_style,
220            with_root,
221        }) => zip_dir(src_path, dest_path, zip_style, with_root.as_deref())?,
222        BuildStep::GenerateInstaller(installer) => {
223            generate_installer(dist_graph, installer, manifest)?
224        }
225        BuildStep::Checksum(ChecksumImpl {
226            checksum,
227            src_path,
228            dest_path,
229            for_artifact,
230        }) => generate_and_write_checksum(
231            manifest,
232            checksum,
233            src_path,
234            dest_path.as_deref(),
235            for_artifact.as_ref(),
236        )?,
237        BuildStep::UnifiedChecksum(UnifiedChecksumStep {
238            checksum,
239            dest_path,
240        }) => generate_unified_checksum(manifest, *checksum, dest_path)?,
241        BuildStep::OmniborArtifactId(OmniborArtifactIdImpl {
242            src_path,
243            dest_path,
244        }) => generate_omnibor_artifact_id(dist_graph, src_path, dest_path)?,
245        BuildStep::GenerateSourceTarball(SourceTarballStep {
246            committish,
247            prefix,
248            target,
249            working_dir,
250        }) => generate_source_tarball(dist_graph, committish, prefix, target, working_dir)?,
251        BuildStep::Extra(target) => run_extra_artifacts_build(dist_graph, target)?,
252        BuildStep::Updater(updater) => fetch_updater(dist_graph, updater)?,
253    };
254    Ok(())
255}
256
257const AXOUPDATER_ASSET_ROOT: &str = "https://github.com/axodotdev/axoupdater/releases";
258const AXOUPDATER_MINIMUM_VERSION: &str = "0.9.0";
259
260fn axoupdater_latest_asset_root() -> String {
261    format!("{AXOUPDATER_ASSET_ROOT}/latest/download")
262}
263
264fn axoupdater_asset_root() -> String {
265    format!("{AXOUPDATER_ASSET_ROOT}/download/v{}", axoupdater::VERSION)
266}
267
268/// Fetches an installer executable and installs it in the expected target path.
269pub fn fetch_updater(dist_graph: &DistGraph, updater: &UpdaterStep) -> DistResult<()> {
270    let ext = if updater.target_triple.is_windows() {
271        ".zip"
272    } else {
273        ".tar.xz"
274    };
275
276    let asset_root = if updater.use_latest {
277        axoupdater_latest_asset_root()
278    } else {
279        axoupdater_asset_root()
280    };
281
282    let expected_url = format!(
283        "{}/axoupdater-cli-{}{ext}",
284        asset_root, updater.target_triple
285    );
286
287    let handle = tokio::runtime::Handle::current();
288    let resp = handle
289        .block_on(dist_graph.axoclient.head(&expected_url))
290        .map_err(|_| DistError::AxoupdaterReleaseCheckFailed {})?;
291
292    // If we have a prebuilt asset, use it
293    if resp.status().is_success() {
294        fetch_updater_from_binary(dist_graph, updater, &expected_url)
295    // If we got a 404, report that there's no binary for this target
296    } else if resp.status() == axoasset::reqwest::StatusCode::NOT_FOUND {
297        Err(DistError::NoAxoupdaterForTarget {
298            target: updater.target_triple.to_string(),
299        })
300    // Some unexpected result that wasn't 200 or 404
301    } else {
302        Err(DistError::AxoupdaterReleaseCheckFailed {})
303    }
304}
305
306/// Creates a temporary directory, returning the directory and
307/// its path as a Utf8PathBuf.
308pub fn create_tmp() -> DistResult<(TempDir, Utf8PathBuf)> {
309    let tmp_dir = TempDir::new()?;
310    let tmp_root =
311        Utf8PathBuf::from_path_buf(tmp_dir.path().to_owned()).expect("tempdir isn't utf8!?");
312    Ok((tmp_dir, tmp_root))
313}
314
315/// Fetches an installer executable from a preexisting binary and installs it in the expected target path.
316fn fetch_updater_from_binary(
317    dist_graph: &DistGraph,
318    updater: &UpdaterStep,
319    asset_url: &str,
320) -> DistResult<()> {
321    let (_tmp_dir, tmp_root) = create_tmp()?;
322    let zipball_target = tmp_root.join("archive");
323
324    let handle = tokio::runtime::Handle::current();
325    handle.block_on(
326        dist_graph
327            .axoclient
328            .load_and_write_to_file(asset_url, &zipball_target),
329    )?;
330    let suffix = if updater.target_triple.is_windows() {
331        ".exe"
332    } else {
333        ""
334    };
335    let requested_filename = format!("axoupdater{suffix}");
336
337    let bytes = if asset_url.ends_with(".tar.xz") {
338        LocalAsset::untar_xz_file(&zipball_target, &requested_filename)?
339    } else if asset_url.ends_with(".tar.gz") {
340        LocalAsset::untar_gz_file(&zipball_target, &requested_filename)?
341    } else if asset_url.ends_with(".zip") {
342        LocalAsset::unzip_file(&zipball_target, &requested_filename)?
343    } else {
344        let extension = Utf8PathBuf::from(asset_url)
345            .extension()
346            .unwrap_or("unable to determine")
347            .to_owned();
348        return Err(DistError::UnrecognizedCompression { extension });
349    };
350
351    let target = dist_graph.target_dir.join(&updater.target_filename);
352    std::fs::write(target, bytes)?;
353
354    Ok(())
355}
356
357fn build_fake(
358    dist_graph: &DistGraph,
359    target: &BuildStep,
360    manifest: &mut DistManifest,
361) -> DistResult<()> {
362    match target {
363        // These two are the meat: don't actually run these at all, just
364        // fake them out
365        BuildStep::Generic(target) => build_fake_generic_target(dist_graph, manifest, target)?,
366        BuildStep::Cargo(target) => build_fake_cargo_target(dist_graph, manifest, target)?,
367        // Never run rustup
368        BuildStep::Rustup(_) => {}
369        // Copying files is fairly safe
370        BuildStep::CopyFile(CopyStep {
371            src_path,
372            dest_path,
373        }) => copy_file(src_path, dest_path)?,
374        BuildStep::CopyDir(CopyStep {
375            src_path,
376            dest_path,
377        }) => copy_dir(src_path, dest_path)?,
378        BuildStep::CopyFileOrDir(CopyStep {
379            src_path,
380            dest_path,
381        }) => copy_file_or_dir(src_path, dest_path)?,
382        // The remainder of these are mostly safe to run as fake steps
383        BuildStep::Zip(ZipDirStep {
384            src_path,
385            dest_path,
386            zip_style,
387            with_root,
388        }) => zip_dir(src_path, dest_path, zip_style, with_root.as_deref())?,
389        BuildStep::GenerateInstaller(installer) => match installer {
390            // MSI and pkg, unlike other installers, aren't safe to generate on any platform
391            InstallerImpl::Msi(msi) => generate_fake_msi(dist_graph, msi, manifest)?,
392            InstallerImpl::Pkg(pkg) => generate_fake_pkg(dist_graph, pkg, manifest)?,
393            _ => generate_installer(dist_graph, installer, manifest)?,
394        },
395        BuildStep::Checksum(ChecksumImpl {
396            checksum,
397            src_path,
398            dest_path,
399            for_artifact,
400        }) => generate_and_write_checksum(
401            manifest,
402            checksum,
403            src_path,
404            dest_path.as_deref(),
405            for_artifact.as_ref(),
406        )?,
407        BuildStep::UnifiedChecksum(UnifiedChecksumStep {
408            checksum,
409            dest_path,
410        }) => generate_unified_checksum(manifest, *checksum, dest_path)?,
411        BuildStep::OmniborArtifactId(OmniborArtifactIdImpl {
412            src_path,
413            dest_path,
414        }) => generate_omnibor_artifact_id(dist_graph, src_path, dest_path)?,
415        // Except source tarballs, which are definitely not okay
416        // We mock these because it requires:
417        // 1. git to be installed;
418        // 2. the app to be a git checkout
419        // The latter case is true during CI, but might not be in other
420        // circumstances. Notably, this fixes our tests during nix's builds,
421        // which runs in an unpacked tarball rather than a git checkout.
422        BuildStep::GenerateSourceTarball(SourceTarballStep {
423            committish,
424            prefix,
425            target,
426            working_dir,
427        }) => generate_fake_source_tarball(dist_graph, committish, prefix, target, working_dir)?,
428        // Or extra artifacts, which may involve real builds
429        BuildStep::Extra(target) => run_fake_extra_artifacts_build(dist_graph, target)?,
430        BuildStep::Updater(_) => unimplemented!(),
431    }
432    Ok(())
433}
434
435fn run_fake_extra_artifacts_build(dist: &DistGraph, target: &ExtraBuildStep) -> DistResult<()> {
436    for artifact in &target.artifact_relpaths {
437        let path = dist.dist_dir.join(artifact);
438        LocalAsset::write_new_all("", &path)?;
439    }
440
441    Ok(())
442}
443
444fn generate_fake_msi(
445    _dist: &DistGraph,
446    msi: &MsiInstallerInfo,
447    _manifest: &DistManifest,
448) -> DistResult<()> {
449    LocalAsset::write_new_all("", &msi.file_path)?;
450
451    Ok(())
452}
453
454fn generate_fake_pkg(
455    _dist: &DistGraph,
456    pkg: &PkgInstallerInfo,
457    _manifest: &DistManifest,
458) -> DistResult<()> {
459    LocalAsset::write_new_all("", &pkg.file_path)?;
460
461    Ok(())
462}
463
464fn generate_omnibor_artifact_id(
465    dist_graph: &DistGraph,
466    src_path: &Utf8Path,
467    dest_path: &Utf8Path,
468) -> DistResult<()> {
469    let omnibor = dist_graph.tools.omnibor()?;
470    let mut cmd = Cmd::new(&omnibor.cmd, "generate an OmniBOR Artifact ID");
471    cmd.arg("artifact")
472        .arg("id")
473        .arg("--format")
474        .arg("short")
475        .arg("--path")
476        .arg(src_path);
477
478    let output = cmd.output()?.stdout;
479    let output = String::from_utf8_lossy(&output);
480
481    LocalAsset::write_new_all(&output, dest_path)?;
482
483    Ok(())
484}
485
486/// Generate a checksum for the src_path to dest_path
487fn generate_and_write_checksum(
488    manifest: &mut DistManifest,
489    checksum: &ChecksumStyle,
490    src_path: &Utf8Path,
491    dest_path: Option<&Utf8Path>,
492    for_artifact: Option<&ArtifactId>,
493) -> DistResult<()> {
494    let output = generate_checksum(checksum, src_path)?;
495    if let Some(dest_path) = dest_path {
496        let name = src_path.file_name().expect("hashing file with no name!?");
497        write_checksum_file(&[(name, &output)], dest_path)?;
498    }
499    if let Some(artifact_id) = for_artifact {
500        if let Some(artifact) = manifest.artifacts.get_mut(artifact_id) {
501            artifact.checksums.insert(checksum.ext().to_owned(), output);
502        }
503    }
504    Ok(())
505}
506
507/// Collect all checksums for all artifacts and write them to a unified checksum file
508fn generate_unified_checksum(
509    manifest: &DistManifest,
510    checksum: ChecksumStyle,
511    dest_path: &Utf8Path,
512) -> DistResult<()> {
513    let expected_checksum_ext = checksum.ext();
514    let mut entries: Vec<(&str, &ChecksumValueRef)> = vec![];
515
516    for artifact in manifest.artifacts.values() {
517        let artifact_name = if let Some(artifact_name) = artifact.name.as_deref() {
518            artifact_name
519        } else {
520            continue;
521        };
522
523        for (checksum_ext, checksum) in &artifact.checksums {
524            if checksum_ext == expected_checksum_ext {
525                entries.push((artifact_name.as_str(), checksum));
526            }
527        }
528    }
529    write_checksum_file(&entries, dest_path)?;
530
531    Ok(())
532}
533
534/// Generate a checksum for the src_path and return it as a string
535fn generate_checksum(checksum: &ChecksumStyle, src_path: &Utf8Path) -> DistResult<ChecksumValue> {
536    info!("generating {checksum:?} for {src_path}");
537    use sha2::Digest;
538    use std::fmt::Write;
539
540    let file_bytes = axoasset::LocalAsset::load_bytes(src_path.as_str())?;
541
542    let hash = match checksum {
543        ChecksumStyle::Sha256 => {
544            let mut hasher = sha2::Sha256::new();
545            hasher.update(&file_bytes);
546            hasher.finalize().as_slice().to_owned()
547        }
548        ChecksumStyle::Sha512 => {
549            let mut hasher = sha2::Sha512::new();
550            hasher.update(&file_bytes);
551            hasher.finalize().as_slice().to_owned()
552        }
553        ChecksumStyle::Sha3_256 => {
554            let mut hasher = sha3::Sha3_256::new();
555            hasher.update(&file_bytes);
556            hasher.finalize().as_slice().to_owned()
557        }
558        ChecksumStyle::Sha3_512 => {
559            let mut hasher = sha3::Sha3_512::new();
560            hasher.update(&file_bytes);
561            hasher.finalize().as_slice().to_owned()
562        }
563        ChecksumStyle::Blake2s => {
564            let mut hasher = blake2::Blake2s256::new();
565            hasher.update(&file_bytes);
566            hasher.finalize().as_slice().to_owned()
567        }
568        ChecksumStyle::Blake2b => {
569            let mut hasher = blake2::Blake2b512::new();
570            hasher.update(&file_bytes);
571            hasher.finalize().as_slice().to_owned()
572        }
573        ChecksumStyle::False => {
574            unreachable!()
575        }
576    };
577    let mut output = String::with_capacity(hash.len() * 2);
578    for byte in hash {
579        write!(&mut output, "{:02x}", byte).unwrap();
580    }
581    Ok(ChecksumValue::new(output))
582}
583
584/// Creates a source code tarball from the git archive from
585/// tag/ref/commit `committish`, with the directory prefix `prefix`,
586/// at the output file `target`.
587fn generate_source_tarball(
588    graph: &DistGraph,
589    committish: &str,
590    prefix: &str,
591    target: &Utf8Path,
592    working_dir: &Utf8Path,
593) -> DistResult<()> {
594    let git = if let Some(tool) = &graph.tools.git {
595        tool.cmd.to_owned()
596    } else {
597        return Err(DistError::ToolMissing {
598            tool: "git".to_owned(),
599        });
600    };
601
602    Cmd::new(git, "generate a source tarball for your project")
603        .arg("archive")
604        .arg(committish)
605        .arg("--format=tar.gz")
606        .arg("--prefix")
607        .arg(prefix)
608        .arg("--output")
609        .arg(target)
610        .current_dir(working_dir)
611        .run()?;
612
613    Ok(())
614}
615
616fn generate_fake_source_tarball(
617    _graph: &DistGraph,
618    _committish: &str,
619    _prefix: &str,
620    target: &Utf8Path,
621    _working_dir: &Utf8Path,
622) -> DistResult<()> {
623    LocalAsset::write_new_all("", target)?;
624
625    Ok(())
626}
627
628/// Write the checksum to dest_path
629fn write_checksum_file(
630    entries: &[(&str, &ChecksumValueRef)],
631    dest_path: &Utf8Path,
632) -> DistResult<()> {
633    // Tools like sha256sum expect a new-line-delimited format of
634    // <checksum> <mode><path>
635    //
636    // * checksum is the checksum in hex
637    // * mode is ` ` for "text" and `*` for "binary" — "text" is for CRLF support, we don't want it.
638    // * path is a relative path to the thing being checksummed (usually just a filename)
639    //
640    // We also make sure there's a trailing newline as is traditional.
641    //
642    // By following this format we support commands like `sha256sum --check sha256.sum`,
643    // both the GNU coreutils and Darwin variants, and also Perl `shasum` utility.
644    let mut contents = String::new();
645    for (file_path, checksum) in entries {
646        use std::fmt::Write;
647        writeln!(&mut contents, "{checksum} *{file_path}",).unwrap();
648    }
649    // leave a trailing newline
650    contents.push('\n');
651
652    axoasset::LocalAsset::write_new(&contents, dest_path)?;
653    Ok(())
654}
655
656/// Initialize the dir for an artifact (and delete the old artifact file).
657fn init_artifact_dir(_dist: &DistGraph, artifact: &Artifact) -> DistResult<()> {
658    // Delete any existing bundle
659    if artifact.file_path.exists() {
660        LocalAsset::remove_file(&artifact.file_path)?;
661    }
662
663    let Some(archive) = &artifact.archive else {
664        // If there's no dir than we're done
665        return Ok(());
666    };
667    info!("recreating artifact dir: {}", archive.dir_path);
668
669    // Clear out the dir we'll build the bundle up in
670    if archive.dir_path.exists() {
671        LocalAsset::remove_dir_all(&archive.dir_path)?;
672    }
673    LocalAsset::create_dir(&archive.dir_path)?;
674
675    Ok(())
676}
677
678pub(crate) fn copy_file(src_path: &Utf8Path, dest_path: &Utf8Path) -> DistResult<()> {
679    LocalAsset::copy_file_to_file(src_path, dest_path)?;
680    Ok(())
681}
682
683pub(crate) fn copy_dir(src_path: &Utf8Path, dest_path: &Utf8Path) -> DistResult<()> {
684    LocalAsset::copy_dir_to_dir(src_path, dest_path)?;
685    Ok(())
686}
687
688pub(crate) fn copy_file_or_dir(src_path: &Utf8Path, dest_path: &Utf8Path) -> DistResult<()> {
689    if src_path.is_dir() {
690        copy_dir(src_path, dest_path)
691    } else {
692        copy_file(src_path, dest_path)
693    }
694}
695
696fn zip_dir(
697    src_path: &Utf8Path,
698    dest_path: &Utf8Path,
699    zip_style: &ZipStyle,
700    with_root: Option<&Utf8Path>,
701) -> DistResult<()> {
702    match zip_style {
703        ZipStyle::Zip => LocalAsset::zip_dir(src_path, dest_path, with_root)?,
704        ZipStyle::Tar(CompressionImpl::Gzip) => {
705            LocalAsset::tar_gz_dir(src_path, dest_path, with_root)?
706        }
707        ZipStyle::Tar(CompressionImpl::Xzip) => {
708            LocalAsset::tar_xz_dir(src_path, dest_path, with_root)?
709        }
710        ZipStyle::Tar(CompressionImpl::Zstd) => {
711            LocalAsset::tar_zstd_dir(src_path, dest_path, with_root)?
712        }
713        ZipStyle::TempDir => {
714            // no-op
715        }
716    }
717    Ok(())
718}
719
720/// Arguments for `dist generate` ([`do_generate`][])
721#[derive(Debug)]
722pub struct GenerateArgs {
723    /// Check whether the output differs without writing to disk
724    pub check: bool,
725    /// Which type(s) of config to generate
726    pub modes: Vec<GenerateMode>,
727}
728
729fn do_generate_preflight_checks(dist: &DistGraph) -> DistResult<()> {
730    // Enforce cargo-dist-version, unless...
731    //
732    // * It's a magic vX.Y.Z-github-BRANCHNAME version,
733    //   which we use for testing against a PR branch. In that case the current_version
734    //   should be irrelevant (so sayeth the person who made and uses this feature).
735    //
736    // * The user passed --allow-dirty to the CLI (probably means it's our own tests)
737    if let Some(desired_version) = &dist.config.dist_version {
738        let current_version: Version = std::env!("CARGO_PKG_VERSION").parse().unwrap();
739        if desired_version != &current_version
740            && !desired_version.pre.starts_with("github-")
741            && !matches!(dist.allow_dirty, DirtyMode::AllowAll)
742        {
743            return Err(DistError::MismatchedDistVersion {
744                config_version: desired_version.to_string(),
745                running_version: current_version.to_string(),
746            });
747        }
748    }
749    if !dist.is_init {
750        return Err(DistError::NeedsInit);
751    }
752
753    Ok(())
754}
755
756/// Generate any scripts which are relevant (impl of `dist generate`)
757pub fn do_generate(cfg: &Config, args: &GenerateArgs) -> DistResult<()> {
758    let (dist, _manifest) = gather_work(cfg)?;
759
760    run_generate(&dist, args)?;
761
762    Ok(())
763}
764
765/// The inner impl of do_generate
766pub fn run_generate(dist: &DistGraph, args: &GenerateArgs) -> DistResult<()> {
767    do_generate_preflight_checks(dist)?;
768
769    // If specific modes are specified, operate *only* on those modes
770    // Otherwise, choose any modes that are appropriate
771    let inferred = args.modes.is_empty();
772    let modes = if inferred {
773        &[GenerateMode::Ci, GenerateMode::Msi]
774    } else {
775        // Check that we're not being told to do a contradiction
776        for &mode in &args.modes {
777            if !dist.allow_dirty.should_run(mode)
778                && matches!(dist.allow_dirty, DirtyMode::AllowList(..))
779            {
780                Err(DistError::ContradictoryGenerateModes {
781                    generate_mode: mode,
782                })?;
783            }
784        }
785        &args.modes[..]
786    };
787
788    // generate everything we need to
789    // HEY! if you're adding a case to this, add it to the inferred list above!
790    for &mode in modes {
791        if dist.allow_dirty.should_run(mode) {
792            match mode {
793                GenerateMode::Ci => {
794                    // If you add a CI backend, call it here
795                    let CiInfo { github } = &dist.ci;
796                    if let Some(github) = github {
797                        if args.check {
798                            github.check(dist)?;
799                        } else {
800                            github.write_to_disk(dist)?;
801                        }
802                    }
803                }
804                GenerateMode::Msi => {
805                    for artifact in &dist.artifacts {
806                        if let ArtifactKind::Installer(InstallerImpl::Msi(msi)) = &artifact.kind {
807                            if args.check {
808                                msi.check_config()?;
809                            } else {
810                                msi.write_config_to_disk()?;
811                            }
812                        }
813                    }
814                }
815            }
816        }
817    }
818
819    Ok(())
820}
821
822/// Run any necessary integrity checks for "primary" commands like build/plan
823///
824/// (This is currently equivalent to `dist generate --check`)
825pub fn check_integrity(cfg: &Config) -> DistResult<()> {
826    // We need to avoid overwriting any parts of configuration from CLI here,
827    // so construct a clean copy of config to run the check generate
828    let check_config = Config {
829        // check the whole system is in a good state
830        tag_settings: TagSettings {
831            needs_coherence: false,
832            // Keeping the tag ensures if dist is run in library mode, we
833            // actually check things in library mode.
834            // If we don't do this, `dist plan --tag={name}-{version} will
835            // always fail if there's no bins.
836            tag: cfg.tag_settings.tag.clone(),
837        },
838        // don't do side-effecting networking
839        create_hosting: false,
840        artifact_mode: ArtifactMode::All,
841        no_local_paths: false,
842        allow_all_dirty: cfg.allow_all_dirty,
843        targets: vec![],
844        ci: vec![],
845        installers: vec![],
846        root_cmd: "check".to_owned(),
847    };
848    let (dist, _manifest) = tasks::gather_work(&check_config)?;
849
850    if let Some(hosting) = &dist.hosting {
851        if hosting.hosts.contains(&config::HostingStyle::Axodotdev) {
852            let mut out = Term::stderr();
853            let info = "INFO:";
854            let message = r"You've enabled Axo Releases, which is currently in Closed Beta.
855If you haven't yet signed up, please join our discord
856(https://discord.gg/ECnWuUUXQk) or message hello@axo.dev to get started!
857";
858
859            writeln!(out, "{} {}", out.style().yellow().apply_to(info), message).unwrap();
860        }
861    }
862
863    run_generate(
864        &dist,
865        &GenerateArgs {
866            modes: vec![],
867            check: true,
868        },
869    )
870}
871
872/// Build a cargo target
873fn generate_installer(
874    dist: &DistGraph,
875    style: &InstallerImpl,
876    manifest: &DistManifest,
877) -> DistResult<()> {
878    match style {
879        InstallerImpl::Shell(info) => {
880            installer::shell::write_install_sh_script(dist, info, manifest)?
881        }
882        InstallerImpl::Powershell(info) => {
883            installer::powershell::write_install_ps_script(dist, info)?
884        }
885        InstallerImpl::Npm(info) => installer::npm::write_npm_project(dist, info)?,
886        InstallerImpl::Homebrew(HomebrewImpl { info, fragments }) => {
887            installer::homebrew::write_homebrew_formula(dist, info, fragments, manifest)?
888        }
889        InstallerImpl::Msi(info) => info.build(dist)?,
890        InstallerImpl::Pkg(info) => info.build()?,
891    }
892    Ok(())
893}
894
895/// Get the default list of targets
896pub fn default_desktop_targets() -> Vec<TripleName> {
897    use crate::platform::targets as t;
898
899    vec![
900        // Everyone can build x64!
901        t::TARGET_X64_LINUX_GNU.to_owned(),
902        t::TARGET_X64_WINDOWS.to_owned(),
903        t::TARGET_X64_MAC.to_owned(),
904        t::TARGET_ARM64_MAC.to_owned(),
905        t::TARGET_ARM64_LINUX_GNU.to_owned(),
906        // that one requires a bit of config (use the `messense/cargo-xwin` image)
907        // t::TARGET_ARM64_WINDOWS.to_owned(),
908    ]
909}
910
911/// Get the list of all known targets
912pub fn known_desktop_targets() -> Vec<TripleName> {
913    use crate::platform::targets as t;
914
915    vec![
916        // Everyone can build x64!
917        t::TARGET_X64_LINUX_GNU.to_owned(),
918        t::TARGET_X64_LINUX_MUSL.to_owned(),
919        t::TARGET_X64_WINDOWS.to_owned(),
920        t::TARGET_X64_MAC.to_owned(),
921        t::TARGET_ARM64_MAC.to_owned(),
922        t::TARGET_ARM64_LINUX_GNU.to_owned(),
923        t::TARGET_ARM64_WINDOWS.to_owned(),
924    ]
925}