cargo_px/
lib.rs

1use std::path::Path;
2use std::time::Instant;
3
4use anyhow::Context;
5use codegen_unit::CodegenUnit;
6use guppy::graph::{PackageGraph, PackageMetadata};
7use targets::determine_targets;
8
9use crate::codegen_unit::{extract_codegen_units, BinaryInvocation};
10
11mod codegen_plan;
12mod codegen_unit;
13mod config;
14mod shell;
15mod targets;
16
17pub use shell::{Shell, Verbosity};
18
19/// Find all codegen units in the current workspace and perform code generation for each of them,
20/// in an order that takes into account their respective dependency relationships.
21#[tracing::instrument(level = tracing::Level::DEBUG, name = "Generate crates", skip(cargo_path))]
22pub fn codegen(
23    cargo_path: &str,
24    working_directory: &Path,
25    args: &[String],
26    shell: &mut Shell,
27) -> Result<(), Vec<anyhow::Error>> {
28    let package_graph = package_graph(cargo_path, shell).map_err(|e| vec![e])?;
29    let codegen_plan = compute_filtered_codegen_plan(working_directory, args, &package_graph)?;
30
31    let workspace_dir = package_graph
32        .workspace()
33        .root()
34        .canonicalize()
35        .context("Failed to get the canonical path to the root directory of this workspace")
36        .map_err(|e| vec![e])?;
37    for unit in codegen_plan {
38        generate_crate(&unit, cargo_path, &workspace_dir, shell).map_err(|e| vec![e])?;
39    }
40
41    Ok(())
42}
43
44/// Find all codegen units in the current workspace and verify that the associated projects
45/// are fresh—i.e. they don't need to be regenerated.
46#[tracing::instrument(level = tracing::Level::DEBUG, name = "Verify freshness", skip(cargo_path))]
47pub fn verify(
48    cargo_path: &str,
49    working_directory: &Path,
50    args: &[String],
51    shell: &mut Shell,
52) -> Result<(), Vec<anyhow::Error>> {
53    let package_graph = package_graph(cargo_path, shell).map_err(|e| vec![e])?;
54    let codegen_plan = compute_filtered_codegen_plan(working_directory, args, &package_graph)?;
55
56    let workspace_dir = package_graph
57        .workspace()
58        .root()
59        .canonicalize()
60        .context("Failed to get the canonical path to the root directory of this workspace")
61        .map_err(|e| vec![e])?;
62    for unit in codegen_plan {
63        let Some(verifier) = &unit.verifier else {
64            return Err(vec![anyhow::anyhow!(
65                "`{}` doesn't define a verifier, therefore we can't verify if it's fresh",
66                unit.package_metadata.name()
67            )]);
68        };
69        verify_crate(
70            verifier,
71            &unit.package_metadata,
72            cargo_path,
73            &workspace_dir,
74            shell,
75        )
76        .map_err(|e| vec![e])?;
77    }
78
79    Ok(())
80}
81
82fn compute_filtered_codegen_plan<'a>(
83    working_directory: &Path,
84    args: &[String],
85    package_graph: &'a PackageGraph,
86) -> Result<Vec<CodegenUnit<'a>>, Vec<anyhow::Error>> {
87    let mut codegen_units = extract_codegen_units(package_graph)?;
88
89    if tracing::event_enabled!(tracing::Level::DEBUG) {
90        let codegen_unit_names: Vec<_> = codegen_units
91            .iter()
92            .map(|unit| unit.package_metadata.name().to_string())
93            .collect();
94        tracing::debug!(
95            ?codegen_unit_names,
96            "Determined the list of codegen units in the current workspace"
97        );
98    }
99
100    let targets = determine_targets(args, working_directory, package_graph);
101
102    if tracing::event_enabled!(tracing::Level::DEBUG) {
103        let target_names: Vec<_> = targets
104            .iter()
105            .map(|id| {
106                package_graph
107                    .metadata(id)
108                    .expect("Unknown package id")
109                    .name()
110                    .to_owned()
111            })
112            .collect();
113        tracing::debug!(
114            ?target_names,
115            "Determined the list of target packages for this invocation"
116        );
117    }
118
119    // Keep only the codegen units that appear in the dependency graph of the targets we've chosen
120    if !targets.is_empty() {
121        let mut depends_cache = package_graph.new_depends_cache();
122        codegen_units.retain(|unit| {
123            targets.iter().any(|target_id| {
124                unit.package_metadata.id() == target_id
125                    || depends_cache
126                        .depends_on(target_id, unit.package_metadata.id())
127                        .unwrap_or(false)
128            })
129        });
130    }
131
132    if tracing::event_enabled!(tracing::Level::DEBUG) {
133        let codegen_unit_names: Vec<_> = codegen_units
134            .iter()
135            .map(|unit| unit.package_metadata.name().to_string())
136            .collect();
137        tracing::debug!(
138            ?codegen_unit_names,
139            "Retaining only the following codegen units for this invocation, based on the target packages"
140        );
141    }
142
143    codegen_plan::codegen_plan(codegen_units, package_graph)
144}
145
146#[tracing::instrument(name = "Verify crate", skip_all, fields(crate_name = %package_metadata.name()))]
147fn verify_crate(
148    verifier: &BinaryInvocation,
149    package_metadata: &PackageMetadata,
150    cargo_path: &str,
151    workspace_path: &Path,
152    shell: &mut Shell,
153) -> Result<(), anyhow::Error> {
154    let be_quiet = shell.verbosity() == Verbosity::Quiet;
155
156    // Compile verifier
157    {
158        let timer = Instant::now();
159        let _ = shell.status(
160            "Compiling",
161            format!(
162                "`{}`, the verifier for `{}`",
163                verifier.binary.name,
164                package_metadata.name()
165            ),
166        );
167        let mut cmd = verifier.build_command(cargo_path, be_quiet);
168        cmd.env("CARGO_PX_WORKSPACE_ROOT_DIR", workspace_path)
169            .stdout(std::process::Stdio::inherit())
170            .stderr(std::process::Stdio::inherit());
171
172        let err_msg = || {
173            format!(
174                "Failed to compile `{}`, the verifier for `{}`",
175                verifier.binary.name,
176                package_metadata.name()
177            )
178        };
179
180        let status = cmd.status().with_context(err_msg)?;
181        if !status.success() {
182            anyhow::bail!(err_msg());
183        }
184        let _ = shell.status(
185            "Compiled",
186            format!(
187                "`{}`, the verifier for `{}`, in {:.3}s",
188                verifier.binary.name,
189                package_metadata.name(),
190                timer.elapsed().as_secs_f32()
191            ),
192        );
193    }
194
195    // Invoke verifier
196    {
197        let timer = Instant::now();
198        let _ = shell.status("Verifying", format!("`{}`", package_metadata.name()));
199        let mut cmd = verifier.run_command(cargo_path, be_quiet);
200
201        cmd.env(
202            "CARGO_PX_GENERATED_PKG_MANIFEST_PATH",
203            package_metadata.manifest_path(),
204        )
205        .env("CARGO_PX_WORKSPACE_ROOT_DIR", workspace_path)
206        .stdout(std::process::Stdio::inherit())
207        .stderr(std::process::Stdio::inherit());
208
209        let err_msg = || {
210            format!(
211                "Failed to run `{}`, the verifier for `{}`",
212                verifier.binary.name,
213                package_metadata.name()
214            )
215        };
216
217        let status = cmd.status().with_context(err_msg)?;
218        if !status.success() {
219            anyhow::bail!(err_msg());
220        }
221        let _ = shell.status(
222            "Verified",
223            format!(
224                "`{}` in {:.3}s",
225                package_metadata.name(),
226                timer.elapsed().as_secs_f32()
227            ),
228        );
229    }
230    Ok(())
231}
232
233#[tracing::instrument(name = "Generate crate", skip_all, fields(crate_name = %unit.package_metadata.name()))]
234fn generate_crate(
235    unit: &codegen_unit::CodegenUnit,
236    cargo_path: &str,
237    workspace_path: &Path,
238    shell: &mut Shell,
239) -> Result<(), anyhow::Error> {
240    let be_quiet = shell.verbosity() == Verbosity::Quiet;
241
242    // Compile generator
243    {
244        let timer = Instant::now();
245        let _ = shell.status(
246            "Compiling",
247            format!(
248                "`{}`, the code generator for `{}`",
249                unit.generator.binary.name,
250                unit.package_metadata.name()
251            ),
252        );
253        let mut cmd = unit.generator.build_command(cargo_path, be_quiet);
254        cmd.env("CARGO_PX_WORKSPACE_ROOT_DIR", workspace_path)
255            .stdout(std::process::Stdio::inherit())
256            .stderr(std::process::Stdio::inherit());
257
258        let err_msg = || {
259            format!(
260                "Failed to compile `{}`, the code generator for `{}`",
261                unit.generator.binary.name,
262                unit.package_metadata.name()
263            )
264        };
265
266        let status = cmd.status().with_context(err_msg)?;
267        if !status.success() {
268            anyhow::bail!(err_msg());
269        }
270        let _ = shell.status(
271            "Compiled",
272            format!(
273                "`{}`, the code generator for `{}`, in {:.3}s",
274                unit.generator.binary.name,
275                unit.package_metadata.name(),
276                timer.elapsed().as_secs_f32()
277            ),
278        );
279    }
280
281    // Invoke generator
282    {
283        let timer = Instant::now();
284        let _ = shell.status("Generating", format!("`{}`", unit.package_metadata.name()));
285        let mut cmd = unit.generator.run_command(cargo_path, be_quiet);
286
287        cmd.env(
288            "CARGO_PX_GENERATED_PKG_MANIFEST_PATH",
289            unit.package_metadata.manifest_path(),
290        )
291        .env("CARGO_PX_WORKSPACE_ROOT_DIR", workspace_path)
292        .stdout(std::process::Stdio::inherit())
293        .stderr(std::process::Stdio::inherit());
294
295        let err_msg = || {
296            format!(
297                "Failed to run `{}`, the code generator for package `{}`",
298                unit.generator.binary.name,
299                unit.package_metadata.name()
300            )
301        };
302
303        let status = cmd.status().with_context(err_msg)?;
304        if !status.success() {
305            anyhow::bail!(err_msg());
306        }
307        let _ = shell.status(
308            "Generated",
309            format!(
310                "`{}` in {:.3}s",
311                unit.package_metadata.name(),
312                timer.elapsed().as_secs_f32()
313            ),
314        );
315    }
316    Ok(())
317}
318
319/// Build the package graph for the current workspace.
320#[tracing::instrument(name = "Compute package graph", skip_all)]
321fn package_graph(cargo_path: &str, shell: &mut Shell) -> Result<PackageGraph, anyhow::Error> {
322    let timer = Instant::now();
323    let _ = shell.status("Computing", "package graph");
324    let mut metadata_cmd = guppy::MetadataCommand::new();
325    metadata_cmd.cargo_path(cargo_path);
326    let package_graph = metadata_cmd
327        .exec()
328        .context("Failed to execute `cargo metadata`")?
329        .build_graph()
330        .context("Failed to build a package graph starting from the output of `cargo metadata`");
331    let _ = shell.status(
332        "Computed",
333        format!("package graph in {:.3}s", timer.elapsed().as_secs_f32()),
334    );
335    package_graph
336}