diff --git a/Cargo.lock b/Cargo.lock index c33de305b..cf79afa76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -746,6 +746,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.10.7" @@ -803,6 +809,7 @@ dependencies = [ "miette 7.5.0", "minijinja", "newline-converter", + "pretty_assertions", "schemars", "semver", "serde", @@ -2068,6 +2075,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "proc-macro2" version = "1.0.83" @@ -3658,6 +3675,12 @@ dependencies = [ "lzma-sys", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yoke" version = "0.7.4" diff --git a/Cargo.toml b/Cargo.toml index f0a01a1f1..71c32cabb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,6 +70,7 @@ current_platform = "0.2.0" color-backtrace = "0.6.1" backtrace = "0.3.74" target-lexicon = "0.12.16" +pretty_assertions = "1.4.1" [workspace.metadata.release] shared-version = true diff --git a/cargo-dist/Cargo.toml b/cargo-dist/Cargo.toml index e671e1362..6541f2778 100644 --- a/cargo-dist/Cargo.toml +++ b/cargo-dist/Cargo.toml @@ -78,6 +78,7 @@ schemars.workspace = true insta.workspace = true tar.workspace = true flate2.workspace = true +pretty_assertions.workspace = true [package.metadata.dist] features = ["fear_no_msrv", "tls_native_roots"] diff --git a/cargo-dist/src/init/apply_dist.rs b/cargo-dist/src/init/apply_dist.rs deleted file mode 100644 index 908fb70d0..000000000 --- a/cargo-dist/src/init/apply_dist.rs +++ /dev/null @@ -1,996 +0,0 @@ -use crate::{platform::MinGlibcVersion, METADATA_DIST}; -use crate::config::{InstallPathStrategy, SystemDependencies}; -use crate::config::v1::{ - artifacts::archives::ArchiveLayer, - artifacts::ArtifactLayer, - builds::BuildLayer, - ci::CiLayer, - hosts::HostLayer, - layer::BoolOr, - publishers::PublisherLayer, - TomlLayer, -}; -use crate::config::v1::installers::{ - homebrew::HomebrewInstallerLayer, msi::MsiInstallerLayer, npm::NpmInstallerLayer, - pkg::PkgInstallerLayer, powershell::PowershellInstallerLayer, - shell::ShellInstallerLayer, CommonInstallerLayer, InstallerLayer, -}; -use axoasset::toml_edit; - -use crate::config::v1::layer::BoolOrOptExt; - -/// Update a workspace toml-edit document with the current DistMetadata value -pub fn apply_dist_to_workspace_toml( - workspace_toml: &mut toml_edit::DocumentMut, - meta: &TomlLayer, -) { - let metadata = workspace_toml.as_item_mut(); - apply_dist_to_metadata(metadata, meta); -} - -/// Ensure [dist] has the given values -pub fn apply_dist_to_metadata(metadata: &mut toml_edit::Item, meta: &TomlLayer) { - let dist_metadata = &mut metadata[METADATA_DIST]; - - // If there's no table, make one - if !dist_metadata.is_table() { - *dist_metadata = toml_edit::table(); - } - - // Apply formatted/commented values - let table = dist_metadata.as_table_mut().unwrap(); - - // This is intentionally written awkwardly to make you update this - let TomlLayer { - config_version, - dist_version, - dist_url_override, - dist, - allow_dirty, - targets, - artifacts, - builds, - ci, - hosts, - installers, - publishers, - } = &meta; - - let installers = &Some(apply_default_install_path(installers)); - - apply_optional_value( - table, - "config-version", - "# The configuration version to use (valid options: 1)\n", - Some(config_version.to_string()), - ); - - apply_optional_value( - table, - "dist-version", - "# The preferred dist version to use in CI (Cargo.toml SemVer syntax)\n", - dist_version.as_ref().map(|v| v.to_string()), - ); - - apply_optional_value( - table, - "dist-url-override", - "# A URL to use to install `cargo-dist` (with the installer script)\n", - dist_url_override.as_ref().map(|v| v.to_string()), - ); - - apply_optional_value( - table, - "dist", - "# Whether the package should be distributed/built by dist (defaults to true)\n", - *dist, - ); - - apply_string_list( - table, - "allow-dirty", - "# Skip checking whether the specified configuration files are up to date\n", - allow_dirty.as_ref(), - ); - - apply_string_list( - table, - "targets", - "# Target platforms to build apps for (Rust target-triple syntax)\n", - targets.as_ref(), - ); - - apply_artifacts(table, artifacts); - apply_builds(table, builds); - apply_ci(table, ci); - apply_hosts(table, hosts); - apply_installers(table, installers); - apply_publishers(table, publishers); - - // TODO(migration): make sure all of these are handled - /* - - - apply_optional_value( - table, - "checksum", - "# Checksums to generate for each App\n", - checksum.map(|c| c.ext().as_str()), - ); - - apply_optional_value( - table, - "merge-tasks", - "# Whether to run otherwise-parallelizable tasks on the same machine\n", - *merge_tasks, - ); - - apply_optional_value( - table, - "fail-fast", - "# Whether failing tasks should make us give up on all other tasks\n", - *fail_fast, - ); - - apply_optional_value( - table, - "cache-builds", - "# Whether builds should try to be cached in CI\n", - *cache_builds, - ); - - apply_optional_value( - table, - "build-local-artifacts", - "# Whether CI should include auto-generated code to build local artifacts\n", - *build_local_artifacts, - ); - - apply_optional_value( - table, - "dispatch-releases", - "# Whether CI should trigger releases with dispatches instead of tag pushes\n", - *dispatch_releases, - ); - - apply_optional_value( - table, - "release-branch", - "# Trigger releases on pushes to this branch instead of tag pushes\n", - release_branch.as_ref(), - ); - - apply_optional_value( - table, - "create-release", - "# Whether dist should create a Github Release or use an existing draft\n", - *create_release, - ); - - apply_optional_value( - table, - "github-release", - "# Which phase dist should use to create the GitHub release\n", - github_release.as_ref().map(|a| a.to_string()), - ); - - apply_optional_value( - table, - "github-releases-repo", - "# Publish GitHub Releases to this repo instead\n", - github_releases_repo.as_ref().map(|a| a.to_string()), - ); - - apply_optional_value( - table, - "github-releases-submodule-path", - "# Read the commit to be tagged from the submodule at this path\n", - github_releases_submodule_path - .as_ref() - .map(|a| a.to_string()), - ); - - apply_string_list( - table, - "plan-jobs", - "# Plan jobs to run in CI\n", - plan_jobs.as_ref(), - ); - - apply_string_list( - table, - "local-artifacts-jobs", - "# Local artifacts jobs to run in CI\n", - local_artifacts_jobs.as_ref(), - ); - - apply_string_list( - table, - "global-artifacts-jobs", - "# Global artifacts jobs to run in CI\n", - global_artifacts_jobs.as_ref(), - ); - - apply_string_list( - table, - "host-jobs", - "# Host jobs to run in CI\n", - host_jobs.as_ref(), - ); - - apply_string_list( - table, - "publish-jobs", - "# Publish jobs to run in CI\n", - publish_jobs.as_ref(), - ); - - apply_string_list( - table, - "post-announce-jobs", - "# Post-announce jobs to run in CI\n", - post_announce_jobs.as_ref(), - ); - - apply_optional_value( - table, - "publish-prereleases", - "# Whether to publish prereleases to package managers\n", - *publish_prereleases, - ); - - apply_optional_value( - table, - "force-latest", - "# Always mark releases as latest, ignoring semver semantics\n", - *force_latest, - ); - - apply_optional_value( - table, - "pr-run-mode", - "# Which actions to run on pull requests\n", - pr_run_mode.as_ref().map(|m| m.to_string()), - ); - - apply_optional_value( - table, - "github-attestations", - "# Whether to enable GitHub Attestations\n", - *github_attestations, - ); - - apply_string_or_list( - table, - "hosting", - "# Where to host releases\n", - hosting.as_ref(), - ); - - apply_optional_value( - table, - "tag-namespace", - "# A prefix git tags must include for dist to care about them\n", - tag_namespace.as_ref(), - ); - - apply_optional_value( - table, - "install-updater", - "# Whether to install an updater program\n", - *install_updater, - ); - - apply_optional_value( - table, - "always-use-latest-updater", - "# Whether to always use the latest updater instead of a specific known-good version\n", - *always_use_latest_updater, - ); - - apply_optional_value( - table, - "display", - "# Whether to display this app's installers/artifacts in release bodies\n", - *display, - ); - - apply_optional_value( - table, - "display-name", - "# Custom display name to use for this app in release bodies\n", - display_name.as_ref(), - ); - - - - */ - - // Finalize the table - table.decor_mut().set_prefix("\n# Config for 'dist'\n"); -} - -fn apply_default_install_path(installers: &Option) -> InstallerLayer { - let mut installers = installers.clone().unwrap_or_default(); - - // Forcibly inline the default install_path if not specified, - // and if we've specified a shell or powershell installer - let install_path = if installers.common.install_path.is_none() - && !(installers.shell.is_none_or_false() || installers.powershell.is_none_or_false()) - { - Some(InstallPathStrategy::default_list()) - } else { - installers.common.install_path.clone() - }; - - installers.common.install_path = install_path; - installers -} - -fn apply_artifacts(table: &mut toml_edit::Table, artifacts: &Option) { - let Some(artifacts) = artifacts else { - return; - }; - let Some(artifacts_table) = table.get_mut("artifacts") else { - return; - }; - let toml_edit::Item::Table(artifacts_table) = artifacts_table else { - panic!("Expected [dist.artifacts] to be a table"); - }; - - // TODO(migration): implement this - - apply_artifacts_archives(artifacts_table, &artifacts.archives); - - apply_optional_value( - artifacts_table, - "source-tarball", - "# Generate and dist a source tarball\n", - artifacts.source_tarball, - ); - - // TODO(migration): implement dist.artifacts.extra. - /* - apply_optional_value( - artifacts_table, - "extra", - "# Any extra artifacts, and their build scripts\n", - artifacts.extra, - ); - */ - - apply_optional_value( - artifacts_table, - "checksum-style", - "# The checksum format to generate\n", - artifacts.checksum.map(|cs| cs.to_string()), - ); - - // Finalize the table - artifacts_table - .decor_mut() - .set_prefix("\n# Artifact configuration for dist\n"); -} - -fn apply_artifacts_archives( - artifacts_table: &mut toml_edit::Table, - archives: &Option, -) { - let Some(archives) = archives else { - return; - }; - let Some(archives_table) = artifacts_table.get_mut("archives") else { - return; - }; - let toml_edit::Item::Table(archives_table) = archives_table else { - panic!("Expected [dist.artifacts.archives] to be a table"); - }; - - apply_string_list( - archives_table, - "include", - "# Extra static files to include in each App (path relative to this Cargo.toml's dir)\n", - archives.include.as_ref(), - ); - - apply_optional_value( - archives_table, - "auto-includes", - "# Whether to auto-include files like READMEs, LICENSEs, and CHANGELOGs (default true)\n", - archives.auto_includes, - ); - - apply_optional_value( - archives_table, - "windows-archive", - "# The archive format to use for windows builds (defaults .zip)\n", - archives.windows_archive.map(|a| a.ext()), - ); - - apply_optional_value( - archives_table, - "unix-archive", - "# The archive format to use for non-windows builds (defaults .tar.xz)\n", - archives.unix_archive.map(|a| a.ext()), - ); - - apply_string_or_list( - archives_table, - "package-libraries", - "# Which kinds of built libraries to include in the final archives\n", - archives.package_libraries.as_ref(), - ); -} - -fn apply_builds(table: &mut toml_edit::Table, builds: &Option) { - let Some(builds) = builds else { - // Nothing to do. - return; - }; - let Some(builds_table) = table.get_mut("builds") else { - // Nothing to do. - return; - }; - let toml_edit::Item::Table(builds_table) = builds_table else { - panic!("Expected [dist.builds] to be a table"); - }; - - apply_optional_value( - builds_table, - "ssldotcom-windows-sign", - "# Whether we should sign Windows binaries using ssl.com", - builds - .ssldotcom_windows_sign - .as_ref() - .map(|p| p.to_string()), - ); - - apply_optional_value( - builds_table, - "macos-sign", - "# Whether to sign macOS executables\n", - builds.macos_sign, - ); - - apply_cargo_builds(builds_table, builds); - apply_system_dependencies(builds_table, builds.system_dependencies.as_ref()); - - apply_optional_min_glibc_version( - builds_table, - "min-glibc-version", - "# The minimum glibc version supported by the package (overrides auto-detection)\n", - builds.min_glibc_version.as_ref(), - ); - - apply_optional_value( - builds_table, - "omnibor", - "# Whether to use omnibor-cli to generate OmniBOR Artifact IDs\n", - builds.omnibor, - ); - - // Finalize the table - builds_table - .decor_mut() - .set_prefix("\n# Build configuration for dist\n"); -} - -fn apply_cargo_builds(builds_table: &mut toml_edit::Table, builds: &BuildLayer) { - if let Some(BoolOr::Bool(b)) = builds.cargo { - // If it was set as a boolean, simply set it as a boolean and return. - apply_optional_value(builds_table, - "cargo", - "# Whether dist should build cargo projects\n# (Use the table format of [dist.builds.cargo] for more nuanced config!)\n", - Some(b), - ); - return; - } - - let Some(BoolOr::Val(ref cargo_builds)) = builds.cargo else { - return; - }; - - let mut possible_table = toml_edit::table(); - let cargo_builds_table = builds_table.get_mut("cargo").unwrap_or(&mut possible_table); - - let toml_edit::Item::Table(cargo_builds_table) = cargo_builds_table else { - panic!("Expected [dist.builds.cargo] to be a table") - }; - - apply_optional_value( - cargo_builds_table, - "rust-toolchain-version", - "# The preferred Rust toolchain to use in CI (rustup toolchain syntax)\n", - cargo_builds.rust_toolchain_version.as_deref(), - ); - - apply_optional_value( - cargo_builds_table, - "msvc-crt-static", - "# Whether +crt-static should be used on msvc\n", - cargo_builds.msvc_crt_static, - ); - - apply_optional_value( - cargo_builds_table, - "precise-builds", - "# Build only the required packages, and individually\n", - cargo_builds.precise_builds, - ); - - apply_string_list( - cargo_builds_table, - "features", - "# Features to pass to cargo build\n", - cargo_builds.features.as_ref(), - ); - - apply_optional_value( - cargo_builds_table, - "default-features", - "# Whether default-features should be enabled with cargo build\n", - cargo_builds.default_features, - ); - - apply_optional_value( - cargo_builds_table, - "all-features", - "# Whether to pass --all-features to cargo build\n", - cargo_builds.all_features, - ); - - apply_optional_value( - cargo_builds_table, - "cargo-auditable", - "# Whether to embed dependency information using cargo-auditable\n", - cargo_builds.cargo_auditable, - ); - - apply_optional_value( - cargo_builds_table, - "cargo-cyclonedx", - "# Whether to use cargo-cyclonedx to generate an SBOM\n", - cargo_builds.cargo_cyclonedx, - ); - - // Finalize the table - cargo_builds_table - .decor_mut() - .set_prefix("\n# How dist should build Cargo projects\n"); -} - -fn apply_system_dependencies( - builds_table: &mut toml_edit::Table, - system_dependencies: Option<&SystemDependencies>, -) { - let Some(system_dependencies) = system_dependencies else { - // Nothing to do. - return; - }; - - // TODO(migration): implement this -} - -fn apply_ci(table: &mut toml_edit::Table, ci: &Option) { - let Some(ci_table) = table.get_mut("ci") else { - // Nothing to do. - return; - }; - let toml_edit::Item::Table(ci_table) = ci_table else { - panic!("Expected [dist.ci] to be a table"); - }; - - // TODO(migration): implement this - - // Finalize the table - ci_table - .decor_mut() - .set_prefix("\n# CI configuration for dist\n"); -} - -fn apply_hosts(table: &mut toml_edit::Table, hosts: &Option) { - let Some(hosts_table) = table.get_mut("hosts") else { - // Nothing to do. - return; - }; - let toml_edit::Item::Table(hosts_table) = hosts_table else { - panic!("Expected [dist.hosts] to be a table"); - }; - - // TODO(migration): implement this - - // Finalize the table - hosts_table - .decor_mut() - .set_prefix("\n# Hosting configuration for dist\n"); -} - -fn apply_installers(table: &mut toml_edit::Table, installers: &Option) { - let Some(installers) = installers else { - return; - }; - let Some(installers_table) = table.get_mut("installers") else { - return; - }; - let toml_edit::Item::Table(installers_table) = installers_table else { - panic!("Expected [dist.installers] to be a table"); - }; - - apply_installers_common(installers_table, &installers.common); - - if let Some(homebrew) = &installers.homebrew { - match homebrew { - BoolOr::Bool(b) => { - apply_optional_value( - installers_table, - "homebrew", - "# Whether to build a Homebrew installer\n", - Some(*b), - ); - } - BoolOr::Val(v) => { - apply_installers_homebrew(installers_table, v); - } - } - } - - if let Some(msi) = &installers.msi { - match msi { - BoolOr::Bool(b) => { - apply_optional_value( - installers_table, - "msi", - "# Whether to build an MSI installer\n", - Some(*b), - ); - } - BoolOr::Val(v) => { - apply_installers_msi(installers_table, v); - } - } - } - - if let Some(npm) = &installers.npm { - match npm { - BoolOr::Bool(b) => { - apply_optional_value( - installers_table, - "npm", - "# Whether to build an NPM installer\n", - Some(*b), - ); - } - BoolOr::Val(v) => { - apply_installers_npm(installers_table, v); - } - } - } - - if let Some(powershell) = &installers.powershell { - match powershell { - BoolOr::Bool(b) => { - apply_optional_value( - installers_table, - "powershell", - "# Whether to build a PowerShell installer\n", - Some(*b), - ); - } - BoolOr::Val(v) => { - apply_installers_powershell(installers_table, v); - } - } - } - - if let Some(shell) = &installers.shell { - match shell { - BoolOr::Bool(b) => { - apply_optional_value( - installers_table, - "shell", - "# Whether to build a Shell installer\n", - Some(*b), - ); - } - BoolOr::Val(v) => { - apply_installers_shell(installers_table, v); - } - } - } - - if let Some(pkg) = &installers.pkg { - match pkg { - BoolOr::Bool(b) => { - apply_optional_value( - installers_table, - "pkg", - "\n# Configuration for the Mac .pkg installer\n", - Some(*b), - ); - } - BoolOr::Val(v) => { - apply_installers_pkg(installers_table, v); - } - } - } - - // installer.updater: Option - // installer.always_use_latest_updater: Option - apply_optional_value( - installers_table, - "updater", - "# Whether to install an updater program alongside the software\n", - installers.updater, - ); - - apply_optional_value( - installers_table, - "always-use-latest-updater", - "# Whether to always use the latest updater version instead of a fixed version\n", - installers.always_use_latest_updater, - ); - - // Finalize the table - installers_table - .decor_mut() - .set_prefix("\n# Installer configuration for dist\n"); -} - -fn apply_installers_common(table: &mut toml_edit::Table, common: &CommonInstallerLayer) { - apply_string_or_list( - table, - "install-path", - "# Path that installers should place binaries in\n", - common.install_path.as_ref(), - ); - - apply_optional_value( - table, - "install-success-msg", - "# Custom message to display on successful install\n", - common.install_success_msg.as_deref(), - ); - - apply_string_or_list( - table, - "install-libraries", - "# Which kinds of packaged libraries to install\n", - common.install_libraries.as_ref(), - ); - - // / Aliases to install binaries as - // TODO(migration): handle `pub bin_aliases: Option>>` -} - -fn apply_installers_homebrew( - installers_table: &mut toml_edit::Table, - homebrew: &HomebrewInstallerLayer, -) { - let Some(homebrew_table) = installers_table.get_mut("homebrew") else { - return; - }; - let toml_edit::Item::Table(homebrew_table) = homebrew_table else { - panic!("Expected [dist.installers.homebrew] to be a table"); - }; - - apply_installers_common(homebrew_table, &homebrew.common); - - apply_optional_value( - homebrew_table, - "tap", - "# A GitHub repo to push Homebrew formulas to\n", - homebrew.tap.clone(), - ); - - apply_optional_value( - homebrew_table, - "formula", - "# Customize the Homebrew formula name\n", - homebrew.formula.clone(), - ); - - // Finalize the table - homebrew_table - .decor_mut() - .set_prefix("\n# Configure the built Homebrew installer\n"); -} - -fn apply_installers_msi(installers_table: &mut toml_edit::Table, msi: &MsiInstallerLayer) { - let Some(msi_table) = installers_table.get_mut("msi") else { - return; - }; - let toml_edit::Item::Table(msi_table) = msi_table else { - panic!("Expected [dist.installers.msi] to be a table"); - }; - - apply_installers_common(msi_table, &msi.common); - - // There are no items under MsiInstallerConfig aside from `msi.common`. - - msi_table - .decor_mut() - .set_prefix("\n# Configure the built MSI installer\n"); -} - -fn apply_installers_npm(installers_table: &mut toml_edit::Table, npm: &NpmInstallerLayer) { - let Some(npm_table) = installers_table.get_mut("npm") else { - return; - }; - let toml_edit::Item::Table(npm_table) = npm_table else { - panic!("Expected [dist.installers.npm] to be a table"); - }; - - apply_installers_common(npm_table, &npm.common); - - apply_optional_value( - npm_table, - "package", - "# The npm package should have this name\n", - npm.package.as_deref(), - ); - - apply_optional_value( - npm_table, - "scope", - "# A namespace to use when publishing this package to the npm registry\n", - npm.scope.as_deref(), - ); -} - -fn apply_installers_powershell( - installers_table: &mut toml_edit::Table, - powershell: &PowershellInstallerLayer, -) { - let Some(powershell_table) = installers_table.get_mut("powershell") else { - return; - }; - let toml_edit::Item::Table(powershell_table) = powershell_table else { - panic!("Expected [dist.installers.powershell] to be a table"); - }; - - apply_installers_common(powershell_table, &powershell.common); - - // TODO(migration): implement this (similar to shell) -} - -fn apply_installers_shell(installers_table: &mut toml_edit::Table, shell: &ShellInstallerLayer) { - let Some(shell_table) = installers_table.get_mut("shell") else { - return; - }; - let toml_edit::Item::Table(shell_table) = shell_table else { - panic!("Expected [dist.installers.shell] to be a table"); - }; - - apply_installers_common(shell_table, &shell.common); - - // TODO(migration): implement this -} - -fn apply_installers_pkg(installers_table: &mut toml_edit::Table, pkg: &PkgInstallerLayer) { - let Some(pkg_table) = installers_table.get_mut("pkg") else { - return; - }; - let toml_edit::Item::Table(pkg_table) = pkg_table else { - panic!("Expected [dist.installers.pkg] to be a table"); - }; - - apply_installers_common(pkg_table, &pkg.common); - - apply_optional_value( - pkg_table, - "identifier", - "# A unique identifier, in tld.domain.package format\n", - pkg.identifier.clone(), - ); - - apply_optional_value( - pkg_table, - "install-location", - "# The location to which software should be installed (defaults to /usr/local)\n", - pkg.install_location.clone(), - ); - - // Finalize the table - pkg_table - .decor_mut() - .set_prefix("\n# Configuration for the Mac .pkg installer\n"); -} - -fn apply_publishers(table: &mut toml_edit::Table, publishers: &Option) { - let Some(publishers_table) = table.get_mut("publishers") else { - return; - }; - let toml_edit::Item::Table(publishers_table) = publishers_table else { - panic!("Expected [dist.publishers] to be a table"); - }; - - // TODO(migration): implement this - - // Finalize the table - publishers_table - .decor_mut() - .set_prefix("\n# Publisher configuration for dist\n"); -} - -/// Update the toml table to add/remove this value -/// -/// If the value is Some we will set the value and hang a description comment off of it. -/// If the given key already existed in the table, this will update it in place and overwrite -/// whatever comment was above it. If the given key is new, it will appear at the end of the -/// table. -/// -/// If the value is None, we delete it (and any comment above it). -fn apply_optional_value(table: &mut toml_edit::Table, key: &str, desc: &str, val: Option) -where - I: Into, -{ - if let Some(val) = val { - table.insert(key, toml_edit::value(val)); - if let Some(mut key) = table.key_mut(key) { - key.leaf_decor_mut().set_prefix(desc) - } - } else { - table.remove(key); - } -} - -/// Same as [`apply_optional_value`][] but with a list of items to `.to_string()` -fn apply_string_list(table: &mut toml_edit::Table, key: &str, desc: &str, list: Option) -where - I: IntoIterator, - I::Item: std::fmt::Display, -{ - if let Some(list) = list { - let items = list.into_iter().map(|i| i.to_string()).collect::>(); - let array: toml_edit::Array = items.into_iter().collect(); - // FIXME: Break the array up into multiple lines with pretty formatting - // if the list is "too long". Alternatively, more precisely toml-edit - // the existing value so that we can preserve the user's formatting and comments. - table.insert(key, toml_edit::Item::Value(toml_edit::Value::Array(array))); - if let Some(mut key) = table.key_mut(key) { - key.leaf_decor_mut().set_prefix(desc) - } - } else { - table.remove(key); - } -} - -/// Same as [`apply_string_list`][] but when the list can be shorthanded as a string -fn apply_string_or_list(table: &mut toml_edit::Table, key: &str, desc: &str, list: Option) -where - I: IntoIterator, - I::Item: std::fmt::Display, -{ - if let Some(list) = list { - let items = list.into_iter().map(|i| i.to_string()).collect::>(); - if items.len() == 1 { - apply_optional_value(table, key, desc, items.into_iter().next()) - } else { - apply_string_list(table, key, desc, Some(items)) - } - } else { - table.remove(key); - } -} - -/// Similar to [`apply_optional_value`][] but specialized to `MinGlibcVersion`, since we're not able to work with structs dynamically -fn apply_optional_min_glibc_version( - table: &mut toml_edit::Table, - key: &str, - desc: &str, - val: Option<&MinGlibcVersion>, -) { - if let Some(min_glibc_version) = val { - let new_item = &mut table[key]; - let mut new_table = toml_edit::table(); - if let Some(new_table) = new_table.as_table_mut() { - for (target, version) in min_glibc_version { - new_table.insert(target, toml_edit::Item::Value(version.to_string().into())); - } - new_table.decor_mut().set_prefix(desc); - } - new_item.or_insert(new_table); - } else { - table.remove(key); - } -} diff --git a/cargo-dist/src/init/apply_dist/artifacts.rs b/cargo-dist/src/init/apply_dist/artifacts.rs new file mode 100644 index 000000000..72a6c0c49 --- /dev/null +++ b/cargo-dist/src/init/apply_dist/artifacts.rs @@ -0,0 +1,186 @@ +use super::helpers::*; +use crate::config::v1::artifacts::archives::ArchiveLayer; +use crate::config::v1::artifacts::ArtifactLayer; +use axoasset::toml_edit::{self, Item, Table}; + +pub fn apply(table: &mut toml_edit::Table, artifacts: &Option) { + let Some(artifacts) = artifacts else { + return; + }; + let artifacts_table = table + .entry("artifacts") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .expect("[dist.artifacts] should be a table"); + + apply_artifacts_archives(artifacts_table, &artifacts.archives); + + apply_optional_value( + artifacts_table, + "source-tarball", + "# Generate a source tarball\n", + artifacts.source_tarball, + ); + + // [dist.artifacts.extra] is not reformatted due to complexity. + skip_optional_value( + artifacts_table, + "extra", + "# Any extra artifacts, and their build scripts\n", + artifacts.extra.as_ref(), + ); + + apply_optional_value( + artifacts_table, + "checksum", + "# The checksum format to generate\n", + artifacts.checksum.map(|cs| cs.to_string()), + ); + + // Finalize the table + artifacts_table + .decor_mut() + .set_prefix("\n# Artifact configuration for dist\n"); +} + +fn apply_artifacts_archives( + artifacts_table: &mut toml_edit::Table, + archives: &Option, +) { + let Some(archives) = archives else { + return; + }; + let archives_table = artifacts_table + .entry("archives") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .expect("[dist.artifacts.archives] should be a table"); + + apply_string_list( + archives_table, + "include", + "# Extra static files to include in each App (path relative to this Cargo.toml's dir)\n", + archives.include.as_ref(), + ); + + apply_optional_value( + archives_table, + "auto-includes", + "# Whether to auto-include files like READMEs, LICENSEs, and CHANGELOGs (default true)\n", + archives.auto_includes, + ); + + apply_optional_value( + archives_table, + "windows-archive", + "# The archive format to use for windows builds (defaults .zip)\n", + archives.windows_archive.map(|a| a.ext()), + ); + + apply_optional_value( + archives_table, + "unix-archive", + "# The archive format to use for non-windows builds (defaults .tar.xz)\n", + archives.unix_archive.map(|a| a.ext()), + ); + + apply_string_or_list( + archives_table, + "package-libraries", + "# Which kinds of built libraries to include in the final archives\n", + archives.package_libraries.as_ref(), + ); +} + +#[cfg(test)] +mod test { + use super::*; + use crate::config::LibraryStyle; + use crate::{ChecksumStyle, CompressionImpl, ZipStyle}; + use miette::IntoDiagnostic; + use pretty_assertions::assert_eq; + + fn source() -> toml_edit::DocumentMut { + let src = axoasset::SourceFile::new("fake-dist-workspace.toml", String::new()); + src.deserialize_toml_edit().into_diagnostic().unwrap() + } + + // Given a DocumentMut, make sure it has a [dist] table, and return + // a reference to that dist table. + fn dist_table(doc: &mut toml_edit::DocumentMut) -> &mut toml_edit::Table { + let dist = doc + .entry("dist") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .unwrap(); + // Don't show the empty top-level [dist]. + dist.set_implicit(true); + // Return the table we just created. + dist + } + + #[test] + fn apply_artifacts_empty() { + let expected = ""; + + let artifacts = Some(ArtifactLayer { + archives: None, + checksum: None, + extra: None, + source_tarball: None, + }); + + let mut doc = source(); + let table = dist_table(&mut doc); + + apply(table, &artifacts); + + let toml_text = table.to_string(); + assert_eq!(toml_text, expected); + } + + #[test] + fn apply_artifacts_everything() { + let expected = r#" +# Artifact configuration for dist +[dist.artifacts] +# Generate a source tarball +source-tarball = false +# The checksum format to generate +checksum = "blake2b" + +[dist.artifacts.archives] +# Extra static files to include in each App (path relative to this Cargo.toml's dir) +include = ["some-include"] +# Whether to auto-include files like READMEs, LICENSEs, and CHANGELOGs (default true) +auto-includes = false +# The archive format to use for windows builds (defaults .zip) +windows-archive = ".tar.gz" +# The archive format to use for non-windows builds (defaults .tar.xz) +unix-archive = ".zip" +# Which kinds of built libraries to include in the final archives +package-libraries = ["cdylib", "cstaticlib"] +"#; + + let artifacts = Some(ArtifactLayer { + archives: Some(ArchiveLayer { + include: Some(vec!["some-include".into()]), + auto_includes: Some(false), + windows_archive: Some(ZipStyle::Tar(CompressionImpl::Gzip)), + unix_archive: Some(ZipStyle::Zip), + package_libraries: Some(vec![LibraryStyle::CDynamic, LibraryStyle::CStatic]), + }), + checksum: Some(ChecksumStyle::Blake2b), + extra: None, + source_tarball: Some(false), + }); + + let mut doc = source(); + let table = dist_table(&mut doc); + + apply(table, &artifacts); + + let toml_text = doc.to_string(); + assert_eq!(expected, toml_text); + } +} diff --git a/cargo-dist/src/init/apply_dist/builds.rs b/cargo-dist/src/init/apply_dist/builds.rs new file mode 100644 index 000000000..79b6110ef --- /dev/null +++ b/cargo-dist/src/init/apply_dist/builds.rs @@ -0,0 +1,330 @@ +use super::helpers::*; +use crate::config::v1::builds::BuildLayer; +use crate::config::v1::layer::BoolOr; +use crate::config::SystemDependencies; +use axoasset::toml_edit::{self, Item, Table}; + +pub fn apply(table: &mut toml_edit::Table, builds: &Option) { + let Some(builds) = builds else { + // Nothing to do. + return; + }; + + let builds_table = table + .entry("builds") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .expect("[dist.builds] should be a table"); + + apply_optional_value( + builds_table, + "ssldotcom-windows-sign", + "# Whether we should sign Windows binaries using ssl.com\n", + builds + .ssldotcom_windows_sign + .as_ref() + .map(|p| p.to_string()), + ); + + apply_optional_value( + builds_table, + "macos-sign", + "# Whether to sign macOS executables\n", + builds.macos_sign, + ); + + apply_cargo_builds(builds_table, builds); + apply_system_dependencies(builds_table, builds.system_dependencies.as_ref()); + + apply_optional_value( + builds_table, + "omnibor", + "# Whether to use omnibor-cli to generate OmniBOR Artifact IDs\n", + builds.omnibor, + ); + + apply_optional_min_glibc_version( + builds_table, + "min-glibc-version", + "\n# The minimum glibc version supported by the package (overrides auto-detection)\n", + builds.min_glibc_version.as_ref(), + ); + + // Finalize the table + builds_table + .decor_mut() + .set_prefix("\n# Build configuration for dist\n"); +} + +pub fn apply_system_dependencies( + _builds_table: &mut toml_edit::Table, + _system_dependencies: Option<&SystemDependencies>, +) { + // This is complex enough we don't support editing it in init, so this does nothing. +} + +fn apply_cargo_builds(builds_table: &mut toml_edit::Table, builds: &BuildLayer) { + if let Some(BoolOr::Bool(b)) = builds.cargo { + // If it was set as a boolean, simply set it as a boolean and return. + apply_optional_value(builds_table, + "cargo", + "# Whether dist should build cargo projects\n# (Use the table format of [dist.builds.cargo] for more nuanced config!)\n", + Some(b), + ); + return; + } + + let Some(BoolOr::Val(ref cargo_builds)) = builds.cargo else { + return; + }; + + let cargo_builds_table = builds_table + .entry("cargo") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .expect("[dist.builds.cargo] should be a bool or a table"); + + apply_optional_value( + cargo_builds_table, + "rust-toolchain-version", + "# The preferred Rust toolchain to use in CI (rustup toolchain syntax)\n", + cargo_builds.rust_toolchain_version.as_deref(), + ); + + apply_optional_value( + cargo_builds_table, + "msvc-crt-static", + "# Whether +crt-static should be used on msvc\n", + cargo_builds.msvc_crt_static, + ); + + apply_optional_value( + cargo_builds_table, + "precise-builds", + "# Build only the required packages, and individually\n", + cargo_builds.precise_builds, + ); + + apply_string_list( + cargo_builds_table, + "features", + "# Features to pass to cargo build\n", + cargo_builds.features.as_ref(), + ); + + apply_optional_value( + cargo_builds_table, + "default-features", + "# Whether default-features should be enabled with cargo build\n", + cargo_builds.default_features, + ); + + apply_optional_value( + cargo_builds_table, + "all-features", + "# Whether to pass --all-features to cargo build\n", + cargo_builds.all_features, + ); + + apply_optional_value( + cargo_builds_table, + "cargo-auditable", + "# Whether to embed dependency information using cargo-auditable\n", + cargo_builds.cargo_auditable, + ); + + apply_optional_value( + cargo_builds_table, + "cargo-cyclonedx", + "# Whether to use cargo-cyclonedx to generate an SBOM\n", + cargo_builds.cargo_cyclonedx, + ); + + // Finalize the table + cargo_builds_table + .decor_mut() + .set_prefix("\n# How dist should build Cargo projects\n"); +} + +#[cfg(test)] +mod test { + use super::*; + use crate::config::v1::builds::cargo::CargoBuildLayer; + use crate::config::v1::builds::generic::GenericBuildLayer; + use crate::config::v1::builds::CommonBuildLayer; + use crate::config::ProductionMode; + use miette::IntoDiagnostic; + use pretty_assertions::assert_eq; + + fn source() -> toml_edit::DocumentMut { + let src = axoasset::SourceFile::new("fake-dist-workspace.toml", String::new()); + src.deserialize_toml_edit().into_diagnostic().unwrap() + } + + // Given a DocumentMut, make sure it has a [dist] table, and return + // a reference to that dist table. + fn dist_table(doc: &mut toml_edit::DocumentMut) -> &mut toml_edit::Table { + let dist = doc + .entry("dist") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .unwrap(); + // Don't show the empty top-level [dist]. + dist.set_implicit(true); + // Return the table we just created. + dist + } + + #[test] + fn apply_empty() { + let expected = ""; + + let layer = Some(BuildLayer { + common: CommonBuildLayer {}, + ssldotcom_windows_sign: None, + macos_sign: None, + cargo: None, + generic: None, + system_dependencies: None, + min_glibc_version: None, + omnibor: None, + }); + + let mut doc = source(); + let table = dist_table(&mut doc); + + apply(table, &layer); + + let toml_text = table.to_string(); + assert_eq!(toml_text, expected); + } + + #[test] + fn apply_everything() { + let expected = r#" +# Build configuration for dist +[dist.builds] +# Whether we should sign Windows binaries using ssl.com +ssldotcom-windows-sign = "test" +# Whether to sign macOS executables +macos-sign = true +# Whether dist should build cargo projects +# (Use the table format of [dist.builds.cargo] for more nuanced config!) +cargo = true +# Whether to use omnibor-cli to generate OmniBOR Artifact IDs +omnibor = true + +# The minimum glibc version supported by the package (overrides auto-detection) +[dist.builds.min-glibc-version] +some-target = "1.2" +"#; + + let mut min_glibc = crate::platform::MinGlibcVersion::new(); + min_glibc.insert( + "some-target".to_string(), + crate::platform::LibcVersion { + major: 1, + series: 2, + }, + ); + + let layer = Some(BuildLayer { + common: CommonBuildLayer {}, + ssldotcom_windows_sign: Some(ProductionMode::Test), + macos_sign: Some(true), + cargo: Some(BoolOr::Bool(true)), + generic: Some(BoolOr::Bool(true)), + system_dependencies: None, + min_glibc_version: Some(min_glibc), + omnibor: Some(true), + }); + + let mut doc = source(); + let table = dist_table(&mut doc); + + apply(table, &layer); + + let toml_text = doc.to_string(); + assert_eq!(expected, toml_text); + } + + #[test] + fn apply_complex() { + let expected = r#" +# Build configuration for dist +[dist.builds] +# Whether we should sign Windows binaries using ssl.com +ssldotcom-windows-sign = "test" +# Whether to sign macOS executables +macos-sign = true +# Whether to use omnibor-cli to generate OmniBOR Artifact IDs +omnibor = true + +# How dist should build Cargo projects +[dist.builds.cargo] +# Whether +crt-static should be used on msvc +msvc-crt-static = true +# Build only the required packages, and individually +precise-builds = true +# Features to pass to cargo build +features = ["some-feature"] +# Whether default-features should be enabled with cargo build +default-features = true +# Whether to pass --all-features to cargo build +all-features = true +# Whether to embed dependency information using cargo-auditable +cargo-auditable = true +# Whether to use cargo-cyclonedx to generate an SBOM +cargo-cyclonedx = true + +# The minimum glibc version supported by the package (overrides auto-detection) +[dist.builds.min-glibc-version] +some-target = "1.2" +"#; + + let mut min_glibc = crate::platform::MinGlibcVersion::new(); + min_glibc.insert( + "some-target".to_string(), + crate::platform::LibcVersion { + major: 1, + series: 2, + }, + ); + + let cargo_bl = CargoBuildLayer { + common: CommonBuildLayer {}, + // Deprecated/v0-specific. + rust_toolchain_version: None, + msvc_crt_static: Some(true), + precise_builds: Some(true), + features: Some(vec!["some-feature".to_string()]), + default_features: Some(true), + all_features: Some(true), + cargo_auditable: Some(true), + cargo_cyclonedx: Some(true), + }; + + let generic_bl = GenericBuildLayer { + common: CommonBuildLayer {}, + }; + + let layer = Some(BuildLayer { + common: CommonBuildLayer {}, + ssldotcom_windows_sign: Some(ProductionMode::Test), + macos_sign: Some(true), + cargo: Some(BoolOr::Val(cargo_bl)), + generic: Some(BoolOr::Val(generic_bl)), + system_dependencies: None, + min_glibc_version: Some(min_glibc), + omnibor: Some(true), + }); + + let mut doc = source(); + let table = dist_table(&mut doc); + + apply(table, &layer); + + let toml_text = doc.to_string(); + assert_eq!(expected, toml_text); + } +} diff --git a/cargo-dist/src/init/apply_dist/ci.rs b/cargo-dist/src/init/apply_dist/ci.rs new file mode 100644 index 000000000..35d0d6fe1 --- /dev/null +++ b/cargo-dist/src/init/apply_dist/ci.rs @@ -0,0 +1,338 @@ +use super::helpers::*; +use crate::config::v1::ci::github::GithubCiLayer; +use crate::config::v1::ci::{CiLayer, CommonCiLayer}; +use crate::config::v1::layer::BoolOr; +use axoasset::toml_edit::{self, Item, Table}; + +pub fn apply(table: &mut toml_edit::Table, ci: &Option) { + let Some(ci) = ci else { + // Nothing to do. + return; + }; + let ci_table = table + .entry("ci") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .expect("[dist.ci] should be a table"); + + apply_ci_common(ci_table, &ci.common); + + if let Some(github) = &ci.github { + match github { + BoolOr::Bool(b) => { + apply_optional_value( + ci_table, + "github", + "# Whether dist should generate workflows for GitHub CI\n", + Some(*b), + ); + } + BoolOr::Val(v) => { + apply_ci_github(ci_table, v); + } + } + } + + // Finalize the table + ci_table + .decor_mut() + .set_prefix("\n# CI configuration for dist\n"); +} + +fn apply_ci_github(ci_table: &mut toml_edit::Table, github: &GithubCiLayer) { + let gh_table = ci_table + .entry("github") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .expect("[dist.ci.github] should be a table"); + + apply_ci_common(gh_table, &github.common); + + // [dist.ci.github.runners] is not reformatted due to complexity. + skip_optional_value( + gh_table, + "runners", + "# Custom GitHub runners, specified as target triples\n", + github.runners.as_ref(), + ); + + // [dist.ci.github.permissions] is not reformatted due to complexity. + skip_optional_value( + gh_table, + "permissions", + "# Custom permissions for jobs\n", + github.permissions.as_ref(), + ); + + apply_optional_value( + gh_table, + "build-setup", + "# Path to a file containing a YAML array of steps, to be performed before 'dist build'\n\ + # KNOWN BUG: https://github.com/axodotdev/cargo-dist/issues/1750\n", + github.build_setup.clone(), + ); + + // Finalize the table + gh_table + .decor_mut() + .set_prefix("\n# Configure generated workflows for GitHub CI\n"); +} + +fn apply_ci_common(table: &mut toml_edit::Table, common: &CommonCiLayer) { + apply_optional_value( + table, + "merge-tasks", + "# Whether to run otherwise-parallelizable tasks on the same machine\n", + common.merge_tasks, + ); + + apply_optional_value( + table, + "fail-fast", + "# Whether failing tasks should make us give up on all other tasks\n", + common.fail_fast, + ); + + apply_optional_value( + table, + "cache-builds", + "# Whether builds should try to be cached in CI\n", + common.cache_builds, + ); + + apply_optional_value( + table, + "build-local-artifacts", + "# Whether CI should include auto-generated code to build local artifacts\n", + common.build_local_artifacts, + ); + + apply_optional_value( + table, + "dispatch-releases", + "# Whether CI should trigger releases with dispatches instead of tag pushes\n", + common.dispatch_releases, + ); + + apply_optional_value( + table, + "release-branch", + "# Trigger releases on pushes to this branch instead of tag pushes\n", + common.release_branch.as_ref(), + ); + + apply_optional_value( + table, + "pr-run-mode", + "# Which actions to run on pull requests\n", + common.pr_run_mode.as_ref().map(|m| m.to_string()), + ); + + apply_optional_value( + table, + "tag-namespace", + "# A prefix git tags must include for dist to care about them\n", + common.tag_namespace.as_ref(), + ); + + apply_string_list( + table, + "plan-jobs", + "# Additional plan jobs to run in CI\n", + common.plan_jobs.as_ref(), + ); + + apply_string_list( + table, + "build-local-jobs", + "# Additional local artifacts jobs to run in CI\n", + common.build_local_jobs.as_ref(), + ); + + apply_string_list( + table, + "build-global-jobs", + "# Additional global artifacts jobs to run in CI\n", + common.build_global_jobs.as_ref(), + ); + + apply_string_list( + table, + "host-jobs", + "# Additional hosts jobs to run in CI\n", + common.host_jobs.as_ref(), + ); + + apply_string_list( + table, + "publish-jobs", + "# Additional publish jobs to run in CI\n", + common.publish_jobs.as_ref(), + ); + + apply_string_list( + table, + "post-announce-jobs", + "# Additional jobs to run in CI, after the announce job finishes\n", + common.post_announce_jobs.as_ref(), + ); +} + +#[cfg(test)] +mod test { + use super::*; + use miette::IntoDiagnostic; + + fn source() -> toml_edit::DocumentMut { + let src = axoasset::SourceFile::new("fake-dist-workspace.toml", String::new()); + src.deserialize_toml_edit().into_diagnostic().unwrap() + } + + // Given a DocumentMut, make sure it has a [dist] table, and return + // a reference to that dist table. + fn dist_table(doc: &mut toml_edit::DocumentMut) -> &mut toml_edit::Table { + let dist = doc + .entry("dist") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .unwrap(); + // Don't show the empty top-level [dist]. + dist.set_implicit(true); + // Return the table we just created. + dist + } + + #[test] + fn apply_ci_empty() { + let expected = ""; + + let ci = Some(CiLayer { + common: CommonCiLayer { + merge_tasks: None, + fail_fast: None, + cache_builds: None, + build_local_artifacts: None, + dispatch_releases: None, + release_branch: None, + pr_run_mode: None, + tag_namespace: None, + plan_jobs: None, + build_local_jobs: None, + build_global_jobs: None, + host_jobs: None, + publish_jobs: None, + post_announce_jobs: None, + }, + github: None, + }); + + let mut doc = source(); + let table = dist_table(&mut doc); + + apply(table, &ci); + + let toml_text = table.to_string(); + assert_eq!(toml_text, expected); + } + + #[test] + fn apply_ci_everything() { + let expected = r#" +# CI configuration for dist +[dist.ci] +# Whether to run otherwise-parallelizable tasks on the same machine +merge-tasks = true +# Whether failing tasks should make us give up on all other tasks +fail-fast = true +# Whether builds should try to be cached in CI +cache-builds = true +# Whether CI should include auto-generated code to build local artifacts +build-local-artifacts = true +# Whether CI should trigger releases with dispatches instead of tag pushes +dispatch-releases = true +# Trigger releases on pushes to this branch instead of tag pushes +release-branch = "main" +# Which actions to run on pull requests +pr-run-mode = "skip" +# A prefix git tags must include for dist to care about them +tag-namespace = "some-namespace" +# Additional plan jobs to run in CI +plan-jobs = ["./plan-job"] +# Additional local artifacts jobs to run in CI +build-local-jobs = ["./build-local-job-1", "./build-local-job-2"] +# Additional global artifacts jobs to run in CI +build-global-jobs = ["./build-global-job"] +# Additional hosts jobs to run in CI +host-jobs = ["./host-job"] +# Additional publish jobs to run in CI +publish-jobs = ["./publish-job"] +# Additional jobs to run in CI, after the announce job finishes +post-announce-jobs = ["./post-announce-job"] +# Whether dist should generate workflows for GitHub CI +github = true +"#; + + let ci = Some(CiLayer { + common: CommonCiLayer { + merge_tasks: Some(true), + fail_fast: Some(true), + cache_builds: Some(true), + build_local_artifacts: Some(true), + dispatch_releases: Some(true), + release_branch: Some("main".to_string()), + pr_run_mode: Some(dist_schema::PrRunMode::Skip), + tag_namespace: Some("some-namespace".to_string()), + plan_jobs: Some(vec!["./plan-job".parse().unwrap()]), + build_local_jobs: Some(vec![ + "./build-local-job-1".parse().unwrap(), + "./build-local-job-2".parse().unwrap(), + ]), + build_global_jobs: Some(vec!["./build-global-job".parse().unwrap()]), + host_jobs: Some(vec!["./host-job".parse().unwrap()]), + publish_jobs: Some(vec!["./publish-job".parse().unwrap()]), + post_announce_jobs: Some(vec!["./post-announce-job".parse().unwrap()]), + }, + github: Some(BoolOr::Bool(true)), + }); + + let mut doc = source(); + let table = dist_table(&mut doc); + + apply(table, &ci); + + let toml_text = doc.to_string(); + assert_eq!(expected, toml_text); + } + + #[test] + fn apply_ci_gh_complex() { + let expected = r#" +# CI configuration for dist +[dist.ci] + +# Configure generated workflows for GitHub CI +[dist.ci.github] +# Path to a file containing a YAML array of steps, to be performed before 'dist build' +# KNOWN BUG: https://github.com/axodotdev/cargo-dist/issues/1750 +build-setup = "some-build-setup" +"#; + + let ci = Some(CiLayer { + common: CommonCiLayer::default(), + github: Some(BoolOr::Val(GithubCiLayer { + common: CommonCiLayer::default(), + build_setup: Some("some-build-setup".to_string()), + permissions: None, + runners: None, + })), + }); + + let mut doc = source(); + let table = dist_table(&mut doc); + + apply(table, &ci); + + let toml_text = doc.to_string(); + assert_eq!(expected, toml_text); + } +} diff --git a/cargo-dist/src/init/apply_dist/helpers.rs b/cargo-dist/src/init/apply_dist/helpers.rs new file mode 100644 index 000000000..5dae1c346 --- /dev/null +++ b/cargo-dist/src/init/apply_dist/helpers.rs @@ -0,0 +1,104 @@ +use crate::platform::MinGlibcVersion; +use axoasset::toml_edit; +use tracing::trace; + +pub fn skip_optional_value( + _table: &mut toml_edit::Table, + key: &str, + _desc: &str, + _val: Option, +) { + trace!("apply_dist/skipping: {}", key); +} + +pub fn skip_string_list( + _table: &mut toml_edit::Table, + key: &str, + _desc: &str, + _list: Option, +) { + trace!("apply_dist/skipping: {}", key); +} + +/// Update the toml table to add/remove this value +/// +/// If the value is Some we will set the value and hang a description comment off of it. +/// If the given key already existed in the table, this will update it in place and overwrite +/// whatever comment was above it. If the given key is new, it will appear at the end of the +/// table. +/// +/// If the value is None, we delete it (and any comment above it). +pub fn apply_optional_value(table: &mut toml_edit::Table, key: &str, desc: &str, val: Option) +where + I: Into, +{ + if let Some(val) = val { + table.insert(key, toml_edit::value(val)); + if let Some(mut key) = table.key_mut(key) { + key.leaf_decor_mut().set_prefix(desc) + } + } else { + table.remove(key); + } +} + +/// Same as [`apply_optional_value`][] but with a list of items to `.to_string()` +pub fn apply_string_list(table: &mut toml_edit::Table, key: &str, desc: &str, list: Option) +where + I: IntoIterator, + I::Item: std::fmt::Display, +{ + if let Some(list) = list { + let items = list.into_iter().map(|i| i.to_string()).collect::>(); + let array: toml_edit::Array = items.into_iter().collect(); + // FIXME: Break the array up into multiple lines with pretty formatting + // if the list is "too long". Alternatively, more precisely toml-edit + // the existing value so that we can preserve the user's formatting and comments. + table.insert(key, toml_edit::Item::Value(toml_edit::Value::Array(array))); + if let Some(mut key) = table.key_mut(key) { + key.leaf_decor_mut().set_prefix(desc) + } + } else { + table.remove(key); + } +} + +/// Same as [`apply_string_list`][] but when the list can be shorthanded as a string +pub fn apply_string_or_list(table: &mut toml_edit::Table, key: &str, desc: &str, list: Option) +where + I: IntoIterator, + I::Item: std::fmt::Display, +{ + if let Some(list) = list { + let items = list.into_iter().map(|i| i.to_string()).collect::>(); + if items.len() == 1 { + apply_optional_value(table, key, desc, items.into_iter().next()) + } else { + apply_string_list(table, key, desc, Some(items)) + } + } else { + table.remove(key); + } +} + +/// Similar to [`apply_optional_value`][] but specialized to `MinGlibcVersion`, since we're not able to work with structs dynamically +pub fn apply_optional_min_glibc_version( + table: &mut toml_edit::Table, + key: &str, + desc: &str, + val: Option<&MinGlibcVersion>, +) { + if let Some(min_glibc_version) = val { + let new_item = &mut table[key]; + let mut new_table = toml_edit::table(); + if let Some(new_table) = new_table.as_table_mut() { + for (target, version) in min_glibc_version { + new_table.insert(target, toml_edit::Item::Value(version.to_string().into())); + } + new_table.decor_mut().set_prefix(desc); + } + new_item.or_insert(new_table); + } else { + table.remove(key); + } +} diff --git a/cargo-dist/src/init/apply_dist/hosts.rs b/cargo-dist/src/init/apply_dist/hosts.rs new file mode 100644 index 000000000..22aa5acd4 --- /dev/null +++ b/cargo-dist/src/init/apply_dist/hosts.rs @@ -0,0 +1,287 @@ +use super::helpers::*; +use crate::config::v1::hosts::HostLayer; +use crate::config::v1::layer::BoolOr; +use axoasset::toml_edit::{self, Item, Table}; + +pub fn apply(table: &mut toml_edit::Table, hosts: &Option) { + let Some(hosts) = hosts else { + // Nothing to do. + return; + }; + + let hosts_table = table + .entry("hosts") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .expect("[dist.hosts] should be a table"); + + // hosts.common is just `CommonHostLayer {}`, so there's nothing to do. + + apply_optional_value( + hosts_table, + "force-latest", + "# Always regard releases as stable (defaults to false)\n", + hosts.force_latest, + ); + + apply_optional_value( + hosts_table, + "display", + "# Whether artifacts/installers for this app should be displayed in release bodies\n", + hosts.display, + ); + + apply_optional_value( + hosts_table, + "display-name", + "# How to refer to the app in release bodies\n", + hosts.display_name.as_ref(), + ); + + apply_github(hosts_table, hosts); + apply_axodotdev(hosts_table, hosts); + + // Finalize the table + hosts_table + .decor_mut() + .set_prefix("\n# Hosting configuration for dist\n"); +} + +fn apply_github(hosts_table: &mut toml_edit::Table, hosts: &HostLayer) { + if let Some(BoolOr::Bool(b)) = hosts.github { + // If it was set as a boolean, simply set it as a boolean and return. + apply_optional_value(hosts_table, + "github", + "# Configuration for GitHub hosting\n# (Use the table format of [dist.hosts.github] for more nuanced config!)\n", + Some(b), + ); + return; + } + + let Some(BoolOr::Val(ref github)) = hosts.github else { + return; + }; + + let gh_table = hosts_table + .entry("github") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .expect("[dist.hosts.github] should be a bool or a table"); + + apply_optional_value( + gh_table, + "create", + "# Whether dist should create the GitHub release (default: true)\n", + github.create, + ); + + apply_optional_value( + gh_table, + "repo", + "# Publish GitHub Releases to this repo instead\n", + github.repo.as_ref().map(|a| a.to_string()), + ); + + apply_optional_value( + gh_table, + "during", + "# Which phase dist should use to create the GitHub release\n", + github.during.as_ref().map(|a| a.to_string()), + ); + + apply_optional_value( + gh_table, + "submodule-path", + "# Read the commit to be tagged from the submodule at this path\n", + github.submodule_path.as_ref().map(|a| a.to_string()), + ); + + apply_optional_value( + gh_table, + "attestations", + "# Whether to enable GitHub Attestations\n", + github.attestations, + ); + + // Finalize the table + gh_table + .decor_mut() + .set_prefix("\n# Configuration for GitHub hosting\n"); +} + +fn apply_axodotdev(hosts_table: &mut toml_edit::Table, hosts: &HostLayer) { + if let Some(BoolOr::Bool(b)) = hosts.axodotdev { + // If it was set as a boolean, simply set it as a boolean and return. + apply_optional_value( + hosts_table, + "axodotdev", + "# Whether to use axo.dev hosting\n", + Some(b), + ); + return; + } + + let Some(BoolOr::Val(ref _axo)) = hosts.axodotdev else { + return; + }; + + // There is no reason for this to ever involve a struct, + // but it does and I don't have time to untangle it. + // + // So: If the table exists, we turn it into `axodotdev=true`. + // + // Theoretically, there's no valid representation of AxodotdevHostLayer + // which isn't empty, so this should never run. + // -@duckinator + apply_optional_value( + hosts_table, + "axodotdev", + "# Whether to use axo.dev hosting\n", + Some(true), + ); +} + +#[cfg(test)] +mod test { + use super::*; + use crate::config::v1::hosts::github::GithubHostLayer; + use crate::config::v1::hosts::CommonHostLayer; + use crate::config::{GithubReleasePhase, GithubRepoPair}; + use miette::IntoDiagnostic; + use pretty_assertions::assert_eq; + + fn source() -> toml_edit::DocumentMut { + let src = axoasset::SourceFile::new("fake-dist-workspace.toml", String::new()); + src.deserialize_toml_edit().into_diagnostic().unwrap() + } + + // Given a DocumentMut, make sure it has a [dist] table, and return + // a reference to that dist table. + fn dist_table(doc: &mut toml_edit::DocumentMut) -> &mut toml_edit::Table { + let dist = doc + .entry("dist") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .unwrap(); + // Don't show the empty top-level [dist]. + dist.set_implicit(true); + // Return the table we just created. + dist + } + + #[test] + fn apply_empty() { + let expected = ""; + + let layer = Some(HostLayer { + common: CommonHostLayer {}, + force_latest: None, + display: None, + display_name: None, + github: None, + axodotdev: None, + }); + + let mut doc = source(); + let table = dist_table(&mut doc); + + apply(table, &layer); + + let toml_text = table.to_string(); + assert_eq!(toml_text, expected); + } + + #[test] + fn apply_everything() { + let expected = r#" +# Hosting configuration for dist +[dist.hosts] +# Always regard releases as stable (defaults to false) +force-latest = true +# Whether artifacts/installers for this app should be displayed in release bodies +display = true +# How to refer to the app in release bodies +display-name = "some-name" +# Configuration for GitHub hosting +# (Use the table format of [dist.hosts.github] for more nuanced config!) +github = true +# Whether to use axo.dev hosting +axodotdev = true +"#; + + let layer = Some(HostLayer { + common: CommonHostLayer {}, + force_latest: Some(true), + display: Some(true), + display_name: Some("some-name".to_string()), + github: Some(BoolOr::Bool(true)), + axodotdev: Some(BoolOr::Bool(true)), + }); + + let mut doc = source(); + let table = dist_table(&mut doc); + + apply(table, &layer); + + let toml_text = doc.to_string(); + assert_eq!(expected, toml_text); + } + + #[test] + fn apply_complex() { + let expected = r#" +# Hosting configuration for dist +[dist.hosts] +# Always regard releases as stable (defaults to false) +force-latest = true +# Whether artifacts/installers for this app should be displayed in release bodies +display = true +# How to refer to the app in release bodies +display-name = "some-name" +# Whether to use axo.dev hosting +axodotdev = true + +# Configuration for GitHub hosting +[dist.hosts.github] +# Whether dist should create the GitHub release (default: true) +create = true +# Publish GitHub Releases to this repo instead +repo = "example-user/example-repo" +# Which phase dist should use to create the GitHub release +during = "auto" +# Read the commit to be tagged from the submodule at this path +submodule-path = "./foo" +# Whether to enable GitHub Attestations +attestations = true +"#; + + let github = GithubHostLayer { + common: CommonHostLayer {}, + create: Some(true), + repo: Some(GithubRepoPair { + owner: "example-user".to_string(), + repo: "example-repo".to_string(), + }), + submodule_path: Some("./foo".into()), + during: Some(GithubReleasePhase::Auto), + attestations: Some(true), + }; + + let layer = Some(HostLayer { + common: CommonHostLayer {}, + force_latest: Some(true), + display: Some(true), + display_name: Some("some-name".to_string()), + github: Some(BoolOr::Val(github)), + axodotdev: Some(BoolOr::Bool(true)), + }); + + let mut doc = source(); + let table = dist_table(&mut doc); + + apply(table, &layer); + + let toml_text = doc.to_string(); + assert_eq!(expected, toml_text); + } +} diff --git a/cargo-dist/src/init/apply_dist/installers.rs b/cargo-dist/src/init/apply_dist/installers.rs new file mode 100644 index 000000000..8b8a6b068 --- /dev/null +++ b/cargo-dist/src/init/apply_dist/installers.rs @@ -0,0 +1,513 @@ +use super::helpers::*; +use crate::config::v1::layer::BoolOr; +use axoasset::toml_edit::{self, Item, Table}; + +use crate::config::v1::installers::{ + homebrew::HomebrewInstallerLayer, msi::MsiInstallerLayer, npm::NpmInstallerLayer, + pkg::PkgInstallerLayer, powershell::PowershellInstallerLayer, shell::ShellInstallerLayer, + CommonInstallerLayer, InstallerLayer, +}; + +pub fn apply(table: &mut toml_edit::Table, installers: &Option) { + let Some(installers) = installers else { + return; + }; + let installers_table = table + .entry("installers") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .expect("[dist.installers] should be a table"); + + apply_installers_common(installers_table, &installers.common); + + if let Some(homebrew) = &installers.homebrew { + match homebrew { + BoolOr::Bool(b) => { + apply_optional_value( + installers_table, + "homebrew", + "# Whether to build a Homebrew installer\n", + Some(*b), + ); + } + BoolOr::Val(v) => { + apply_installers_homebrew(installers_table, v); + } + } + } + + if let Some(msi) = &installers.msi { + match msi { + BoolOr::Bool(b) => { + apply_optional_value( + installers_table, + "msi", + "# Whether to build an MSI installer\n", + Some(*b), + ); + } + BoolOr::Val(v) => { + apply_installers_msi(installers_table, v); + } + } + } + + if let Some(npm) = &installers.npm { + match npm { + BoolOr::Bool(b) => { + apply_optional_value( + installers_table, + "npm", + "# Whether to build an NPM installer\n", + Some(*b), + ); + } + BoolOr::Val(v) => { + apply_installers_npm(installers_table, v); + } + } + } + + if let Some(powershell) = &installers.powershell { + match powershell { + BoolOr::Bool(b) => { + apply_optional_value( + installers_table, + "powershell", + "# Whether to build a PowerShell installer\n", + Some(*b), + ); + } + BoolOr::Val(v) => { + apply_installers_powershell(installers_table, v); + } + } + } + + if let Some(shell) = &installers.shell { + match shell { + BoolOr::Bool(b) => { + apply_optional_value( + installers_table, + "shell", + "# Whether to build a Shell installer\n", + Some(*b), + ); + } + BoolOr::Val(v) => { + apply_installers_shell(installers_table, v); + } + } + } + + if let Some(pkg) = &installers.pkg { + match pkg { + BoolOr::Bool(b) => { + apply_optional_value( + installers_table, + "pkg", + "# Whether to build a Mac .pkg installer\n", + Some(*b), + ); + } + BoolOr::Val(v) => { + apply_installers_pkg(installers_table, v); + } + } + } + + // installer.updater: Option + // installer.always_use_latest_updater: Option + apply_optional_value( + installers_table, + "updater", + "# Whether to install an updater program alongside the software\n", + installers.updater, + ); + + apply_optional_value( + installers_table, + "always-use-latest-updater", + "# Whether to always use the latest updater version instead of a fixed version\n", + installers.always_use_latest_updater, + ); + + // Finalize the table + installers_table + .decor_mut() + .set_prefix("\n# Installer configuration for dist\n"); +} + +fn apply_installers_common(table: &mut toml_edit::Table, common: &CommonInstallerLayer) { + apply_string_or_list( + table, + "install-path", + "# Path that installers should place binaries in\n", + common.install_path.as_ref(), + ); + + apply_optional_value( + table, + "install-success-msg", + "# Custom message to display on successful install\n", + common.install_success_msg.as_deref(), + ); + + apply_string_or_list( + table, + "install-libraries", + "# Which kinds of packaged libraries to install\n", + common.install_libraries.as_ref(), + ); + + // [dist.ci.installers.bin-aliases] is not reformatted due to complexity. + skip_string_list( + table, + "bin-aliases", + "# Aliases to install for generated binaries\n", + common.bin_aliases.as_ref(), + ); +} + +fn apply_installers_homebrew( + installers_table: &mut toml_edit::Table, + homebrew: &HomebrewInstallerLayer, +) { + let homebrew_table = installers_table + .entry("homebrew") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .expect("[dist.installers.homebrew] should be a table"); + + apply_installers_common(homebrew_table, &homebrew.common); + + apply_optional_value( + homebrew_table, + "tap", + "# A GitHub repo to push Homebrew formulas to\n", + homebrew.tap.clone(), + ); + + apply_optional_value( + homebrew_table, + "formula", + "# Customize the Homebrew formula name\n", + homebrew.formula.clone(), + ); + + // Finalize the table + homebrew_table + .decor_mut() + .set_prefix("\n# Configuration for the Homebrew installer\n"); +} + +fn apply_installers_msi(installers_table: &mut toml_edit::Table, msi: &MsiInstallerLayer) { + let msi_table = installers_table + .entry("msi") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .expect("[dist.installers.msi] should be a table"); + + apply_installers_common(msi_table, &msi.common); + + // There are no items under MsiInstallerConfig aside from `msi.common`. + + msi_table + .decor_mut() + .set_prefix("\n# Configuration for the MSI installer\n"); +} + +fn apply_installers_npm(installers_table: &mut toml_edit::Table, npm: &NpmInstallerLayer) { + let npm_table = installers_table + .entry("npm") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .expect("[dist.installers.npm] should be a table"); + + apply_installers_common(npm_table, &npm.common); + + apply_optional_value( + npm_table, + "package", + "# The name of the npm package\n", + npm.package.as_deref(), + ); + + apply_optional_value( + npm_table, + "scope", + "# The namespace to use when publishing this package to the npm registry\n", + npm.scope.as_deref(), + ); + + npm_table + .decor_mut() + .set_prefix("\n# Configuration for the NPM installer\n"); +} + +fn apply_installers_powershell( + installers_table: &mut toml_edit::Table, + powershell: &PowershellInstallerLayer, +) { + let powershell_table = installers_table + .entry("powershell") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .expect("[dist.installers.powershell] should be a table"); + + apply_installers_common(powershell_table, &powershell.common); + + // Finalize the table + installers_table + .decor_mut() + .set_prefix("\n# Configuration for the Windows PowerShell installer\n"); +} + +fn apply_installers_shell(installers_table: &mut toml_edit::Table, shell: &ShellInstallerLayer) { + let shell_table = installers_table + .entry("shell") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .expect("[dist.installers.shell] should be a table"); + + apply_installers_common(shell_table, &shell.common); + + // Finalize the table + installers_table + .decor_mut() + .set_prefix("\n# Configuration for the *nix shell installer\n"); +} + +fn apply_installers_pkg(installers_table: &mut toml_edit::Table, pkg: &PkgInstallerLayer) { + let pkg_table = installers_table + .entry("pkg") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .expect("[dist.installers.pkg] should be a table"); + + apply_installers_common(pkg_table, &pkg.common); + + apply_optional_value( + pkg_table, + "identifier", + "# A unique identifier, in tld.domain.package format\n", + pkg.identifier.clone(), + ); + + apply_optional_value( + pkg_table, + "install-location", + "# The location to which software should be installed (defaults to /usr/local)\n", + pkg.install_location.clone(), + ); + + // Finalize the table + pkg_table + .decor_mut() + .set_prefix("\n# Configuration for the Mac .pkg installer\n"); +} + +#[cfg(test)] +mod test { + use super::*; + use crate::config::LibraryStyle; + use crate::init::apply_dist::InstallPathStrategy; + use miette::IntoDiagnostic; + use pretty_assertions::assert_eq; + + fn source() -> toml_edit::DocumentMut { + let src = axoasset::SourceFile::new("fake-dist-workspace.toml", String::new()); + src.deserialize_toml_edit().into_diagnostic().unwrap() + } + + // Given a DocumentMut, make sure it has a [dist] table, and return + // a reference to that dist table. + fn dist_table(doc: &mut toml_edit::DocumentMut) -> &mut toml_edit::Table { + let dist = doc + .entry("dist") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .unwrap(); + // Don't show the empty top-level [dist]. + dist.set_implicit(true); + // Return the table we just created. + dist + } + + #[test] + fn apply_installers_empty() { + let expected = ""; + + let installers = Some(InstallerLayer { + common: CommonInstallerLayer { + install_path: None, + install_success_msg: None, + install_libraries: None, + bin_aliases: None, + }, + homebrew: None, + msi: None, + npm: None, + powershell: None, + shell: None, + pkg: None, + updater: None, + always_use_latest_updater: None, + }); + + let mut doc = source(); + let table = dist_table(&mut doc); + + apply(table, &installers); + + let toml_text = table.to_string(); + assert_eq!(toml_text, expected); + } + + #[test] + fn apply_installers_everything_bools() { + let expected = r#" +# Installer configuration for dist +[dist.installers] +# Path that installers should place binaries in +install-path = ["~/some-install-path/", "CARGO_HOME"] +# Custom message to display on successful install +install-success-msg = "default success message" +# Which kinds of packaged libraries to install +install-libraries = ["cdylib", "cstaticlib"] +# Whether to build a Homebrew installer +homebrew = true +# Whether to build an MSI installer +msi = true +# Whether to build an NPM installer +npm = true +# Whether to build a PowerShell installer +powershell = true +# Whether to build a Shell installer +shell = true +# Whether to build a Mac .pkg installer +pkg = true +# Whether to install an updater program alongside the software +updater = true +# Whether to always use the latest updater version instead of a fixed version +always-use-latest-updater = true +"#; + + let installers = Some(InstallerLayer { + common: CommonInstallerLayer { + install_path: Some(vec![ + InstallPathStrategy::HomeSubdir { + subdir: "some-install-path/".to_string(), + }, + InstallPathStrategy::CargoHome, + ]), + install_success_msg: Some("default success message".to_string()), + install_libraries: Some(vec![LibraryStyle::CDynamic, LibraryStyle::CStatic]), + bin_aliases: None, + }, + homebrew: Some(BoolOr::Bool(true)), + msi: Some(BoolOr::Bool(true)), + npm: Some(BoolOr::Bool(true)), + powershell: Some(BoolOr::Bool(true)), + shell: Some(BoolOr::Bool(true)), + pkg: Some(BoolOr::Bool(true)), + updater: Some(true), + always_use_latest_updater: Some(true), + }); + + let mut doc = source(); + let table = dist_table(&mut doc); + + apply(table, &installers); + + let toml_text = doc.to_string(); + assert_eq!(expected, toml_text); + } + + #[test] + fn apply_installers_complex() { + let expected = r#" +# Installer configuration for dist +[dist.installers] +# Path that installers should place binaries in +install-path = ["~/some-install-path/", "CARGO_HOME"] +# Custom message to display on successful install +install-success-msg = "default success message" +# Which kinds of packaged libraries to install +install-libraries = ["cdylib", "cstaticlib"] +# Whether to build an MSI installer +msi = true +# Whether to build a PowerShell installer +powershell = true +# Whether to build a Shell installer +shell = true +# Whether to install an updater program alongside the software +updater = true +# Whether to always use the latest updater version instead of a fixed version +always-use-latest-updater = true + +# Configuration for the Homebrew installer +[dist.installers.homebrew] +# A GitHub repo to push Homebrew formulas to +tap = "homebrew-tap" +# Customize the Homebrew formula name +formula = "homebrew-formula" + +# Configuration for the NPM installer +[dist.installers.npm] +# The name of the npm package +package = "npm-package" +# The namespace to use when publishing this package to the npm registry +scope = "npm-scope" + +# Configuration for the Mac .pkg installer +[dist.installers.pkg] +# A unique identifier, in tld.domain.package format +identifier = "pkg-identifier" +# The location to which software should be installed (defaults to /usr/local) +install-location = "pkg-install-location" +"#; + + let installers = Some(InstallerLayer { + common: CommonInstallerLayer { + install_path: Some(vec![ + InstallPathStrategy::HomeSubdir { + subdir: "some-install-path/".to_string(), + }, + InstallPathStrategy::CargoHome, + ]), + install_success_msg: Some("default success message".to_string()), + install_libraries: Some(vec![LibraryStyle::CDynamic, LibraryStyle::CStatic]), + bin_aliases: None, + }, + homebrew: Some(BoolOr::Val(HomebrewInstallerLayer { + common: CommonInstallerLayer::default(), + tap: Some("homebrew-tap".to_string()), + formula: Some("homebrew-formula".to_string()), + })), + msi: Some(BoolOr::Bool(true)), + npm: Some(BoolOr::Val(NpmInstallerLayer { + common: CommonInstallerLayer::default(), + package: Some("npm-package".to_string()), + scope: Some("npm-scope".to_string()), + })), + powershell: Some(BoolOr::Bool(true)), + shell: Some(BoolOr::Bool(true)), + pkg: Some(BoolOr::Val(PkgInstallerLayer { + common: CommonInstallerLayer::default(), + identifier: Some("pkg-identifier".to_string()), + install_location: Some("pkg-install-location".to_string()), + })), + updater: Some(true), + always_use_latest_updater: Some(true), + }); + + let mut doc = source(); + let table = dist_table(&mut doc); + + apply(table, &installers); + + let toml_text = doc.to_string(); + assert_eq!(expected, toml_text); + } +} diff --git a/cargo-dist/src/init/apply_dist/mod.rs b/cargo-dist/src/init/apply_dist/mod.rs new file mode 100644 index 000000000..b89d12a87 --- /dev/null +++ b/cargo-dist/src/init/apply_dist/mod.rs @@ -0,0 +1,122 @@ +use crate::config::v1::installers::InstallerLayer; +use crate::config::v1::layer::BoolOrOptExt; +use crate::config::v1::TomlLayer; +use crate::config::InstallPathStrategy; +use crate::METADATA_DIST; +use axoasset::toml_edit; + +mod artifacts; +mod builds; +mod ci; +mod helpers; +mod hosts; +mod installers; +mod publishers; + +use helpers::*; + +/// Update a workspace toml-edit document with the current DistMetadata value +pub fn apply_dist_to_workspace_toml(workspace_toml: &mut toml_edit::DocumentMut, meta: &TomlLayer) { + let metadata = workspace_toml.as_item_mut(); + apply_dist_to_metadata(metadata, meta); +} + +/// Ensure [dist] has the given values +pub fn apply_dist_to_metadata(metadata: &mut toml_edit::Item, meta: &TomlLayer) { + let dist_metadata = &mut metadata[METADATA_DIST]; + + // If there's no table, make one + if !dist_metadata.is_table() { + *dist_metadata = toml_edit::table(); + } + + // Apply formatted/commented values + let table = dist_metadata.as_table_mut().unwrap(); + + // This is intentionally written awkwardly to make you update this + let TomlLayer { + config_version, + dist_version, + dist_url_override, + dist, + allow_dirty, + targets, + artifacts, + builds, + ci, + hosts, + installers, + publishers, + } = &meta; + + let installers = &Some(apply_default_install_path(installers)); + + apply_optional_value( + table, + "config-version", + "# The configuration version to use (valid options: 1)\n", + Some(config_version.to_string()), + ); + + apply_optional_value( + table, + "dist-version", + "# The preferred dist version to use in CI (Cargo.toml SemVer syntax)\n", + dist_version.as_ref().map(|v| v.to_string()), + ); + + apply_optional_value( + table, + "dist-url-override", + "# A URL to use to install `cargo-dist` (with the installer script)\n", + dist_url_override.as_ref().map(|v| v.to_string()), + ); + + apply_optional_value( + table, + "dist", + "# Whether the package should be distributed/built by dist (defaults to true)\n", + *dist, + ); + + apply_string_list( + table, + "allow-dirty", + "# Skip checking whether the specified configuration files are up to date\n", + allow_dirty.as_ref(), + ); + + apply_string_list( + table, + "targets", + "# Target platforms to build apps for (Rust target-triple syntax)\n", + targets.as_ref(), + ); + + artifacts::apply(table, artifacts); + builds::apply(table, builds); + ci::apply(table, ci); + hosts::apply(table, hosts); + installers::apply(table, installers); + publishers::apply(table, publishers); + + // Finalize the table + table.decor_mut().set_prefix("\n# Config for 'dist'\n"); +} + +fn apply_default_install_path(installers: &Option) -> InstallerLayer { + let mut installers = installers.clone().unwrap_or_default(); + + // Forcibly inline the default install_path if not specified, + // and if we've specified a shell or powershell installer + let install_path = if installers.common.install_path.is_none() + && !(installers.shell.is_none_or_false() || installers.powershell.is_none_or_false()) + { + Some(InstallPathStrategy::default_list()) + } else { + installers.common.install_path.clone() + }; + + installers.common.install_path = install_path; + installers +} diff --git a/cargo-dist/src/init/apply_dist/publishers.rs b/cargo-dist/src/init/apply_dist/publishers.rs new file mode 100644 index 000000000..48ed34c7b --- /dev/null +++ b/cargo-dist/src/init/apply_dist/publishers.rs @@ -0,0 +1,216 @@ +use super::helpers::*; +use crate::config::v1::layer::BoolOr; +use crate::config::v1::publishers::{CommonPublisherLayer, PublisherLayer}; +use axoasset::toml_edit::{self, Item, Table}; + +pub fn apply(table: &mut toml_edit::Table, publishers: &Option) { + let Some(publishers) = publishers else { + // Nothing to do. + return; + }; + + let publishers_table = table + .entry("publishers") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .expect("[dist.publishers] should be a table"); + + apply_common(publishers_table, &publishers.common); + apply_homebrew(publishers_table, publishers); + apply_npm(publishers_table, publishers); + + // Finalize the table + publishers_table + .decor_mut() + .set_prefix("\n# Publisher configuration for dist\n"); +} + +fn apply_common(table: &mut toml_edit::Table, common: &CommonPublisherLayer) { + apply_optional_value( + table, + "prereleases", + "# Whether to publish prereleases (defaults to false)\n", + common.prereleases, + ); +} + +fn apply_homebrew(publishers_table: &mut toml_edit::Table, publishers: &PublisherLayer) { + if let Some(BoolOr::Bool(b)) = publishers.homebrew { + // If it was set as a boolean, simply set it as a boolean and return. + apply_optional_value( + publishers_table, + "homebrew", + "# Whether to publish to Homebrew\n", + Some(b), + ); + return; + } + + let Some(BoolOr::Val(ref homebrew)) = publishers.homebrew else { + // dist.publishers.homebrew isn't specified; nothing to do. + return; + }; + + let hb_table = publishers_table + .entry("homebrew") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .expect("[dist.publishers.homebrew] should be a bool or a table"); + + apply_common(hb_table, &homebrew.common); + + // Finalize the table + publishers_table + .decor_mut() + .set_prefix("\n# Configuration for publishing to Homebrew\n"); +} + +fn apply_npm(publishers_table: &mut toml_edit::Table, publishers: &PublisherLayer) { + if let Some(BoolOr::Bool(b)) = publishers.npm { + // If it was set as a boolean, simply set it as a boolean and return. + apply_optional_value( + publishers_table, + "npm", + "# Whether to publish to NPM\n", + Some(b), + ); + return; + } + + let Some(BoolOr::Val(ref npm)) = publishers.npm else { + // dist.publishers.npm isn't specified; nothing to do. + return; + }; + + let hb_table = publishers_table + .entry("npm") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .expect("[dist.publishers.npm] should be a bool or a table"); + + apply_common(hb_table, &npm.common); + + // Finalize the table + publishers_table + .decor_mut() + .set_prefix("\n# Configuration for publishing to NPM\n"); +} + +#[cfg(test)] +mod test { + use super::*; + use crate::config::v1::publishers::homebrew::HomebrewPublisherLayer; + use crate::config::v1::publishers::npm::NpmPublisherLayer; + use miette::IntoDiagnostic; + use pretty_assertions::assert_eq; + + fn source() -> toml_edit::DocumentMut { + let src = axoasset::SourceFile::new("fake-dist-workspace.toml", String::new()); + src.deserialize_toml_edit().into_diagnostic().unwrap() + } + + // Given a DocumentMut, make sure it has a [dist] table, and return + // a reference to that dist table. + fn dist_table(doc: &mut toml_edit::DocumentMut) -> &mut toml_edit::Table { + let dist = doc + .entry("dist") + .or_insert(Item::Table(Table::new())) + .as_table_mut() + .unwrap(); + // Don't show the empty top-level [dist]. + dist.set_implicit(true); + // Return the table we just created. + dist + } + + #[test] + fn apply_empty() { + let expected = ""; + + let layer = Some(PublisherLayer { + common: CommonPublisherLayer { prereleases: None }, + homebrew: None, + npm: None, + }); + + let mut doc = source(); + let table = dist_table(&mut doc); + + apply(table, &layer); + + let toml_text = table.to_string(); + assert_eq!(toml_text, expected); + } + + #[test] + fn apply_everything() { + let expected = r#" +# Publisher configuration for dist +[dist.publishers] +# Whether to publish prereleases (defaults to false) +prereleases = true +# Whether to publish to Homebrew +homebrew = true +# Whether to publish to NPM +npm = true +"#; + + let layer = Some(PublisherLayer { + common: CommonPublisherLayer { + prereleases: Some(true), + }, + homebrew: Some(BoolOr::Bool(true)), + npm: Some(BoolOr::Bool(true)), + }); + + let mut doc = source(); + let table = dist_table(&mut doc); + + apply(table, &layer); + + let toml_text = doc.to_string(); + assert_eq!(expected, toml_text); + } + + #[test] + fn apply_complex() { + let expected = r#" +# Publisher configuration for dist +[dist.publishers] +# Whether to publish prereleases (defaults to false) +prereleases = true + +[dist.publishers.homebrew] +# Whether to publish prereleases (defaults to false) +prereleases = true + +[dist.publishers.npm] +# Whether to publish prereleases (defaults to false) +prereleases = true +"#; + + let layer = Some(PublisherLayer { + common: CommonPublisherLayer { + prereleases: Some(true), + }, + homebrew: Some(BoolOr::Val(HomebrewPublisherLayer { + common: CommonPublisherLayer { + prereleases: Some(true), + }, + })), + npm: Some(BoolOr::Val(NpmPublisherLayer { + common: CommonPublisherLayer { + prereleases: Some(true), + }, + })), + }); + + let mut doc = source(); + let table = dist_table(&mut doc); + + apply(table, &layer); + + let toml_text = doc.to_string(); + assert_eq!(expected, toml_text); + } +} diff --git a/cargo-dist/src/init/mod.rs b/cargo-dist/src/init/mod.rs index f5e7dbb56..fda5d4352 100644 --- a/cargo-dist/src/init/mod.rs +++ b/cargo-dist/src/init/mod.rs @@ -1,8 +1,17 @@ pub(crate) mod v0; pub use v0::do_init; +mod apply_dist; pub mod console_helpers; mod dist_profile; mod init_args; +/* +use crate::config::{self, v1::TomlLayer, Config}; +use crate::errors::DistResult; +use crate::migrate; +use crate::SortedMap; +use crate::{do_generate, GenerateArgs}; +use console_helpers::theme; +*/ pub use dist_profile::init_dist_profile; pub use init_args::InitArgs;