debcargo 2.8.3

Create a Debian package from a Cargo crate.
Documentation
use std::path::PathBuf;

use anyhow::Context;

use clap::{crate_version, Parser};

use crate::config::{Config, PackageKey};
use crate::crates::{AutoPatches, CrateInfo};
use crate::debian::{self, DebInfo};
use crate::errors::Result;
use crate::util;

pub struct PackageProcess {
    // below state is filled in during init
    pub crate_info: CrateInfo,
    pub deb_info: DebInfo,
    pub config_path: Option<PathBuf>,
    pub config: Config,
    // below state is filled in during the process
    /// Output directory as specified by the user.
    pub output_dir: Option<PathBuf>,
    /// Whether the crate tarball was modified by exclusions
    pub source_modified: bool,
    /// Automatically generated patches, such as for Cargo.toml normalization
    pub auto_patches: AutoPatches,
    /// Tempdir that contains a working copy of the eventual output.
    pub temp_output_dir: Option<tempfile::TempDir>,
    pub orig_tarball: Option<PathBuf>,
}

#[derive(Debug, Clone, Parser)]
pub struct PackageInitArgs {
    /// Name of the crate to package.
    pub crate_name: String,
    /// Version of the crate to package; may contain dependency operators.
    /// If empty string or omitted, resolves to the latest version.
    pub version: Option<String>,
    /// TOML file providing package-specific options.
    #[arg(long)]
    pub config: Option<PathBuf>,
}

#[derive(Debug, Clone, Parser)]
pub struct PackageExtractArgs {
    /// Output directory for the package. The orig tarball is named according
    /// to Debian conventions in the parent directory of this directory.
    #[arg(long)]
    pub directory: Option<PathBuf>,
}

#[derive(Debug, Clone, Parser)]
pub struct PackageExecuteArgs {
    /// Assume the changelog is already bumped, and leave it alone.
    #[arg(long)]
    pub changelog_ready: bool,
    /// Guess extra values for d/copyright. Might be slow.
    #[arg(long)]
    pub copyright_guess_harder: bool,
    /// Don't write back hint files or d/changelog to the source overlay directory.
    #[arg(long)]
    pub no_overlay_write_back: bool,
}

impl PackageProcess {
    /// More fine-grained access. For normal usage see `Self::init` instead.
    pub fn new(
        mut crate_info: CrateInfo,
        config_path: Option<PathBuf>,
        config: Config,
    ) -> Result<Self> {
        crate_info.set_includes_excludes(config.orig_tar_excludes(), config.orig_tar_whitelist());
        let deb_info = DebInfo::new(
            &crate_info,
            crate_version!(),
            config.semver_suffix,
            config.repack_suffix(),
        );

        Ok(Self {
            crate_info,
            deb_info,
            config_path,
            config,
            output_dir: None,
            source_modified: false,
            auto_patches: AutoPatches::new(),
            temp_output_dir: None,
            orig_tarball: None,
        })
    }

    pub fn init(init_args: PackageInitArgs) -> Result<Self> {
        let crate_name = &init_args.crate_name;
        let version = init_args.version.as_deref();
        let config = init_args.config;

        let (config_path, config) = match config {
            Some(path) => {
                let config = Config::parse(&path).context("failed to parse debcargo.toml")?;
                (Some(path), config)
            }
            None => (None, Config::default()),
        };

        let crate_path = config.crate_src_path(config_path.as_deref());
        let crate_info = match crate_path {
            Some(p) => CrateInfo::new_with_local_crate(crate_name, version, &p)?,
            None => CrateInfo::new(crate_name, version)?,
        };

        Self::new(crate_info, config_path, config)
    }

    pub fn extract(&mut self, extract: PackageExtractArgs) -> Result<()> {
        assert!(self.output_dir.is_none());
        let Self {
            crate_info,
            deb_info,
            config,
            ..
        } = self;
        // vars read; begin stage

        let output_dir = extract
            .directory
            .unwrap_or_else(|| deb_info.package_source_dir().to_path_buf());

        let (source_modified, manifest_normalization_patch) =
            crate_info.extract_crate(&output_dir)?;

        if source_modified && config.repack_suffix().is_none() {
            debcargo_bail!("orig tarball has been modified, but no repack_suffix set -> please set repack_suffix in debcargo.toml!");
        }

        let mut auto_patches = AutoPatches::new();
        if let Some(patch) = manifest_normalization_patch {
            auto_patches.insert(
                "auto-cargo-toml-normalization.patch".to_string(),
                patch.into_bytes(),
            );
        }

        // Removing features must not modify the orig tarball, so it is shipped
        // as a quilt patch on top of any earlier auto-patches (e.g. Cargo.toml
        // normalization). The in-memory manifest picks up the removed features
        // later, when apply_overlay_and_patches applies the patches and reloads
        // Cargo.toml.
        if let Some(disabled) = config.remove_features.as_deref() {
            if !disabled.is_empty() {
                let toml_path = output_dir.join("Cargo.toml");
                crate::crates::remove_features_patch(&toml_path, disabled, &mut auto_patches)?;
            }
        }

        // stage finished; set vars
        self.output_dir = Some(output_dir);
        self.source_modified = source_modified;
        self.auto_patches = auto_patches;
        Ok(())
    }

    pub fn apply_overrides(&mut self) -> Result<()> {
        assert!(self.temp_output_dir.is_none());
        let Self {
            crate_info,
            config_path,
            config,
            output_dir,
            auto_patches,
            ..
        } = self;
        let output_dir = output_dir.as_ref().unwrap();
        // vars read; begin stage

        let temp_output_dir = debian::apply_overlay_and_patches(
            crate_info,
            config_path.as_deref(),
            config,
            output_dir,
            auto_patches,
        )?;

        // stage finished; set vars
        self.temp_output_dir = Some(temp_output_dir);
        Ok(())
    }

    pub fn prepare_orig_tarball(&mut self) -> Result<()> {
        assert!(self.orig_tarball.is_none());
        let Self {
            crate_info,
            deb_info,
            output_dir,
            source_modified,
            ..
        } = self;
        let output_dir = output_dir.as_ref().unwrap();
        // vars read; begin stage

        let orig_tarball = output_dir
            .parent()
            .unwrap()
            .join(deb_info.orig_tarball_path());
        debian::prepare_orig_tarball(crate_info, &orig_tarball, *source_modified)?;

        // stage finished; set vars
        self.orig_tarball = Some(orig_tarball);
        Ok(())
    }

    pub fn prepare_debian_folder(&mut self, args: &PackageExecuteArgs) -> Result<()> {
        let Self {
            crate_info,
            deb_info,
            config_path,
            config,
            output_dir,
            temp_output_dir,
            auto_patches,
            ..
        } = self;
        let output_dir = output_dir.as_ref().unwrap();
        let temp_output_dir = temp_output_dir.as_ref().unwrap();
        // vars read; begin stage

        debian::prepare_debian_folder(
            crate_info,
            deb_info,
            config_path.as_deref(),
            config,
            output_dir,
            temp_output_dir,
            auto_patches,
            args.changelog_ready,
            args.copyright_guess_harder,
            !args.no_overlay_write_back,
        )?;

        // stage finished; set vars
        Ok(())
    }

    pub fn post_package_checks(&self) -> Result<()> {
        let Self {
            config_path,
            config,
            output_dir,
            orig_tarball,
            ..
        } = self;
        let output_dir = output_dir.as_ref().unwrap();
        let orig_tarball = orig_tarball.as_ref().unwrap();

        let curdir = std::env::current_dir()?;
        debcargo_info!(
            concat!("Package Source: {}\n", "Original Tarball for package: {}\n"),
            util::rel_p(output_dir, &curdir),
            util::rel_p(orig_tarball, &curdir)
        );
        let fixmes = util::lookup_fixmes(output_dir)?;
        if !fixmes.is_empty() {
            debcargo_warn!("FIXME found in the following files.");
            for f in fixmes {
                if util::hint_file_for(&f).is_some() {
                    debcargo_warn!("\t(•) {}", util::rel_p(&f, &curdir));
                } else {
                    debcargo_warn!("\t •  {}", util::rel_p(&f, &curdir));
                }
            }
            debcargo_warn!("");
            debcargo_warn!("To fix, try combinations of the following: ");
            match config_path.as_deref() {
                None => debcargo_warn!("\t •  Write a config file and use it with --config"),
                Some(c) => {
                    debcargo_warn!("\t •  Add or edit overrides in your config file:");
                    debcargo_warn!("\t    {}", util::rel_p(c, &curdir));
                }
            }
            match config.overlay_dir(config_path.as_deref()) {
                None => debcargo_warn!("\t •  Create an overlay directory and add it to your config file with overlay = \"/path/to/overlay\""),
                Some(p) => {
                    debcargo_warn!("\t •  Add or edit files in your overlay directory:");
                    debcargo_warn!("\t    {}", util::rel_p(&p, &curdir));
                }
            }
        }
        if let Some(architecture) = self.config.package_architecture(PackageKey::BareLib) {
            if architecture == &vec!["all"] {
                debcargo_warn!("");
                debcargo_warn!(
                    "'Architecture: all' override found for library package. This potentially breaks Multi-Arch,"
                );
                debcargo_warn!(
                    "see https://rust-team.pages.debian.net/book/policy.html#why-architecture-any."
                );
            }
        }
        Ok(())
    }
}