feature-manifest 0.4.0

Document, validate, and render Cargo feature metadata.
Documentation
use std::fs;
use std::path::{Path, PathBuf};

use anyhow::{Context, Result};

use crate::{
    InjectionMarkers, MetadataLayout, PackageSelection, SyncOptions, WorkspaceManifest,
    ensure_injection_markers, inject_between_markers, load_workspace, render_markdown,
    sync_manifest,
};

#[derive(Debug, Clone)]
pub struct InitOptions {
    pub manifest_path: PathBuf,
    pub selection: PackageSelection,
    pub readme: Option<PathBuf>,
    pub no_readme: bool,
    pub ci: bool,
    pub style: Option<MetadataLayout>,
}

pub fn run(workspace: &WorkspaceManifest, options: InitOptions) -> Result<()> {
    let root_directory = workspace
        .root_manifest_path
        .parent()
        .unwrap_or_else(|| Path::new("."));

    for package in &workspace.packages {
        let report = sync_manifest(
            &package.manifest_path,
            &SyncOptions {
                check_only: false,
                remove_stale: false,
                style: options.style,
            },
        )?;
        let package_name = report.package_name.as_deref().unwrap_or("unknown-package");
        if report.changed() {
            println!("initialized metadata for `{package_name}`");
        } else {
            println!("metadata for `{package_name}` is already initialized");
        }
    }

    let refreshed_workspace = load_workspace(&options.manifest_path, options.selection)?;

    if !options.no_readme {
        let readme_path = options
            .readme
            .unwrap_or_else(|| root_directory.join("README.md"));
        let markers = InjectionMarkers::default();
        ensure_injection_markers(&readme_path, &markers, "## Features")?;
        inject_between_markers(
            &readme_path,
            &render_markdown(&refreshed_workspace, false),
            &markers,
        )?;
        println!(
            "updated README feature section at `{}`",
            readme_path.display()
        );
    }

    if options.ci {
        let workflow_path = root_directory
            .join(".github")
            .join("workflows")
            .join("feature-manifest.yml");
        write_ci_workflow(&workflow_path)?;
        println!("added CI workflow at `{}`", workflow_path.display());
    }

    println!("next: cargo fm");
    Ok(())
}

fn write_ci_workflow(path: &Path) -> Result<()> {
    if path.exists() {
        return Ok(());
    }

    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent).with_context(|| {
            format!("failed to create workflow directory `{}`", parent.display())
        })?;
    }

    fs::write(path, CI_WORKFLOW)
        .with_context(|| format!("failed to write CI workflow `{}`", path.display()))
}

const CI_WORKFLOW: &str = r#"name: Feature Manifest

on:
  push:
    branches:
      - main
  pull_request:

jobs:
  feature-manifest:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - name: Install feature-manifest
        run: cargo install feature-manifest --locked
      - name: Check feature metadata
        run: cargo fm
      - name: Check generated README section
        run: cargo fm md --check -i README.md
"#;