python_proto_importer/commands/
build.rs

1use crate::config::{AppConfig, Backend};
2use crate::generator::protoc::ProtocRunner;
3use crate::postprocess::add_pyright_header;
4use crate::postprocess::apply::apply_rewrites_in_tree;
5use crate::postprocess::create_packages;
6use crate::postprocess::fds::{collect_generated_basenames_from_bytes, load_fds_from_bytes};
7use crate::postprocess::rel_imports::scan_and_report;
8use crate::verification::import_test::verify;
9use anyhow::{Context, Result};
10use std::path::Path;
11
12/// Execute the build command to generate Python code from proto files.
13///
14/// This is the main entry point for the code generation pipeline. It performs
15/// the complete workflow: loading configuration, generating code, applying
16/// post-processing transformations, and running verification checks.
17///
18/// # Arguments
19///
20/// * `pyproject` - Optional path to the pyproject.toml file. If None, uses "pyproject.toml"
21/// * `no_verify` - If true, skips the verification step after generation
22/// * `_postprocess_only` - If true, skips generation and only runs post-processing (experimental)
23///
24/// # Returns
25///
26/// Returns `Ok(())` on successful completion, or an error if any step fails.
27///
28/// # Pipeline Steps
29///
30/// 1. **Configuration**: Load and validate pyproject.toml settings
31/// 2. **Generation**: Run protoc or buf to generate Python files
32/// 3. **Post-processing**:
33///    - Create `__init__.py` files if configured
34///    - Convert absolute imports to relative imports
35///    - Add type checker suppression headers
36/// 4. **Verification**: Run import tests and optional type checking
37///
38/// # Example
39///
40/// ```no_run
41/// use python_proto_importer::commands::build;
42///
43/// // Standard build
44/// build(None, false, false)?;
45///
46/// // Build without verification
47/// build(None, true, false)?;
48///
49/// // Build with custom config file
50/// build(Some("custom.toml"), false, false)?;
51/// # Ok::<(), anyhow::Error>(())
52/// ```
53pub fn build(pyproject: Option<&str>, no_verify: bool, _postprocess_only: bool) -> Result<()> {
54    let cfg = AppConfig::load(pyproject.map(Path::new)).context("failed to load config")?;
55    tracing::info!(?cfg.backend, out=%cfg.out.display(), "build start");
56
57    let allowed_basenames = if _postprocess_only {
58        if !cfg.out.exists() {
59            anyhow::bail!(
60                "--postprocess-only: output directory does not exist: {}",
61                cfg.out.display()
62            );
63        }
64        tracing::info!("postprocess-only mode: skip generation");
65        None
66    } else {
67        match cfg.backend {
68            Backend::Protoc => {
69                let runner = ProtocRunner::new(&cfg);
70                let fds_bytes = runner.generate()?;
71                let _pool = load_fds_from_bytes(&fds_bytes).context("decode FDS failed")?;
72                Some(
73                    collect_generated_basenames_from_bytes(&fds_bytes)
74                        .context("collect basenames from FDS failed")?,
75                )
76            }
77            Backend::Buf => {
78                tracing::warn!("buf backend is not implemented yet");
79                None
80            }
81        }
82    };
83
84    if cfg.postprocess.create_package {
85        let created = create_packages(&cfg.out)?;
86        tracing::info!("created __init__.py: {}", created);
87    }
88
89    let (files, hits) =
90        scan_and_report(&cfg.out).context("scan relative-import candidates failed")?;
91    tracing::info!(
92        "relative-import candidates: files={}, lines={}",
93        files,
94        hits
95    );
96
97    if cfg.postprocess.relative_imports {
98        let modified = apply_rewrites_in_tree(
99            &cfg.out,
100            cfg.postprocess.exclude_google,
101            &cfg.postprocess.module_suffixes,
102            allowed_basenames.as_ref(),
103        )
104        .context("apply relative-import rewrites failed")?;
105        tracing::info!(
106            "relative-import rewrites applied: {} files modified",
107            modified
108        );
109    }
110
111    if cfg.postprocess.pyright_header {
112        let added = add_pyright_header(&cfg.out)?;
113        if added > 0 {
114            tracing::info!("pyright header added: {} files", added);
115        }
116    }
117
118    if !no_verify {
119        verify(&cfg)?;
120    }
121    Ok(())
122}