Skip to main content

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