pint_cli/
build.rs

1//! `pint build` implementation.
2
3use anyhow::Context;
4use clap::builder::styling::Style;
5use pint_pkg::{
6    build::{BuiltPkg, BuiltPkgs},
7    manifest::{ManifestFile, PackageKind},
8    plan::Plan,
9};
10use std::{
11    collections::HashMap,
12    path::{Path, PathBuf},
13};
14
15/// Build a package, writing the generated artifacts to `out/`.
16#[derive(clap::Args, Debug)]
17pub struct Args {
18    /// The path to the package manifest.
19    ///
20    /// If not provided, the current directory is checked and then each parent
21    /// recursively until a manifest is found.
22    #[arg(long = "manifest-path")]
23    manifest_path: Option<PathBuf>,
24    /// A 256-bit unsigned integer in hexadeciaml format that represents the contract "salt". The
25    /// value is left padded with zeros if it has less than 64 hexadecimal digits.
26    ///
27    /// The "salt" is hashed along with the contract's bytecode in order to make the address of the
28    /// contract unique.
29    ///
30    /// If "salt" is provided for a library package, an error is emitted.
31    #[arg(long, value_parser = parse_hex)]
32    salt: Option<[u8; 32]>,
33    /// Print the parsed package.
34    #[arg(long = "print-parsed")]
35    print_parsed: bool,
36    /// Print the flattened package.
37    #[arg(long = "print-flat")]
38    print_flat: bool,
39    /// Print the optimized package.
40    #[arg(long = "print-optimized")]
41    print_optimized: bool,
42    /// Print the assembly generated for the package.
43    #[arg(long = "print-asm")]
44    print_asm: bool,
45    /// Skip optimizing the package.
46    #[arg(long = "skip-optimize", hide = true)]
47    skip_optimize: bool,
48    /// Don't print anything that wasn't explicitly requested.
49    #[arg(long)]
50    silent: bool,
51}
52
53/// Parses a `&str` that represents a 256-bit unsigned integer in hexadecimal format and converts
54/// it into a `[u8; 32]`. If the string has less than 64 hexadecimal digits, left pad with zeros.
55///
56/// Emits an error if the conversion is not possible.
57fn parse_hex(value: &str) -> Result<[u8; 32], String> {
58    if value.len() > 64 || !value.chars().all(|c| c.is_ascii_hexdigit()) {
59        return Err("Salt must be a hexadecimal number with up to 64 digts (256 bits)".to_string());
60    }
61
62    // Pad the value to 64 characters by prepending zeros if needed
63    let padded_value = format!("{:0>64}", value);
64    let mut salt = [0u8; 32];
65    for i in 0..32 {
66        salt[i] = u8::from_str_radix(&padded_value[2 * i..2 * i + 2], 16)
67            .map_err(|_| "Invalid hexadecimal value")?;
68    }
69    Ok(salt)
70}
71
72// Find the file within the current directory or parent directories with the given name.
73fn find_file(mut dir: PathBuf, file_name: &str) -> Option<PathBuf> {
74    loop {
75        let path = dir.join(file_name);
76        if path.exists() {
77            return Some(path);
78        }
79        if !dir.pop() {
80            return None;
81        }
82    }
83}
84
85/// Build a pint package or workspace given a set of `build` args.
86///
87/// Returns the build [`Plan`] that was used, along with the set of packages that were built.
88pub fn cmd(args: Args) -> anyhow::Result<(Plan, BuiltPkgs)> {
89    let build_start = std::time::Instant::now();
90
91    // Determine the manifest location.
92    let manifest_path = match args.manifest_path {
93        Some(path) => path,
94        None => {
95            let current_dir = std::env::current_dir()?;
96            match find_file(current_dir, ManifestFile::FILE_NAME) {
97                None => anyhow::bail!("no `pint.toml` in the current or parent directories"),
98                Some(path) => path,
99            }
100        }
101    };
102
103    // Prepare some ANSI formatting styles for output.
104    let bold = Style::new().bold();
105
106    // Prepare the compilation plan.
107    let manifest = ManifestFile::from_path(&manifest_path).context("failed to load manifest")?;
108    if let PackageKind::Library = manifest.pkg.kind {
109        if args.salt.is_some() {
110            anyhow::bail!("specifying `salt` for a library package is not allowed");
111        }
112    }
113
114    let name = manifest.pkg.name.to_string();
115    let members = [(name, manifest.clone())].into_iter().collect();
116    // TODO: Print fetching process here when remote deps included.
117    let plan = pint_pkg::plan::from_members(&members).context("failed to plan compilation")?;
118
119    // Build the given compilation plan.
120    let mut builder = pint_pkg::build::build_plan(&plan);
121    let options = pint_pkg::build::BuildOptions {
122        salts: HashMap::from_iter([(manifest.clone(), args.salt.unwrap_or_default())]),
123        print_parsed: args.print_parsed,
124        print_flat: args.print_flat,
125        print_optimized: args.print_optimized,
126        print_asm: args.print_asm,
127        skip_optimize: args.skip_optimize,
128    };
129
130    while let Some(prebuilt) = builder.next_pkg() {
131        let pinned = prebuilt.pinned();
132        let manifest = &plan.manifests()[&pinned.id()];
133        let source_str = source_string(pinned, manifest.dir());
134
135        if !args.silent {
136            println!(
137                "   {}Compiling{} {} [{}] ({})",
138                bold.render(),
139                bold.render_reset(),
140                pinned.name,
141                manifest.pkg.kind,
142                source_str,
143            );
144        }
145
146        // Build the package.
147        let _built = match prebuilt.build(&options) {
148            Ok(built) => {
149                built.print_warnings();
150                built
151            }
152            Err(err) => {
153                let msg = format!("{}", err.kind);
154                err.print_diagnostics();
155                anyhow::bail!("{msg}");
156            }
157        };
158    }
159
160    // Consume the builder and produce the built pkgs.
161    let built_pkgs = builder.into_built_pkgs();
162
163    // Write our built member package to the `out/` directory.
164    if let Some(&n) = plan.compilation_order().last() {
165        let built = &built_pkgs[&n];
166        let pinned = &plan.graph()[n];
167        let manifest = &plan.manifests()[&pinned.id()];
168
169        // Create the output and profile directories.
170        // TODO: Add build profiles with compiler params.
171        let out_dir = manifest.out_dir();
172        let profile = "debug";
173        let profile_dir = out_dir.join(profile);
174        std::fs::create_dir_all(&profile_dir)
175            .with_context(|| format!("failed to create directory {profile_dir:?}"))?;
176
177        // Write the output artifacts to the directory.
178        built
179            .write_to_dir(&pinned.name, &profile_dir)
180            .with_context(|| format!("failed to write output artifacts to {profile_dir:?}"))?;
181
182        if !args.silent {
183            // Print the build summary.
184            println!(
185                "    {}Finished{} build [{profile}] in {:?}",
186                bold.render(),
187                bold.render_reset(),
188                build_start.elapsed()
189            );
190        }
191
192        // Print the build summary for our member package.
193        let kind_str = format!("{}", manifest.pkg.kind);
194        let padded_kind_str = format!("{kind_str:>12}");
195        let padding = &padded_kind_str[..padded_kind_str.len() - kind_str.len()];
196        let ca = match built {
197            BuiltPkg::Contract(contract) => format!("{}", &contract.ca),
198            _ => "".to_string(),
199        };
200        let name_col_w = name_col_w(&pinned.name, built);
201
202        if !args.silent {
203            println!(
204                "{padding}{}{kind_str}{} {:<name_col_w$} {}",
205                bold.render(),
206                bold.render_reset(),
207                pinned.name,
208                ca,
209            );
210        }
211
212        // For contracts, print their predicates too.
213        if let BuiltPkg::Contract(contract) = built {
214            let mut iter = contract.predicate_metadata.iter().peekable();
215            while let Some(predicate) = iter.next() {
216                let pred_name = summary_predicate_name(&predicate.name);
217                let name = format!("{}{}", pinned.name, pred_name);
218                let pipe = iter.peek().map(|_| "├──").unwrap_or("└──");
219                if !args.silent {
220                    println!("         {pipe} {:<name_col_w$} {}", name, predicate.ca);
221                }
222            }
223        }
224    }
225
226    Ok((plan, built_pkgs))
227}
228
229// Package name formatted (including source if not a member).
230fn source_string(pinned: &pint_pkg::plan::Pinned, manifest_dir: &Path) -> String {
231    match pinned.source {
232        pint_pkg::source::Pinned::Member(_) => {
233            format!("{}", manifest_dir.display())
234        }
235        _ => format!("{}", pinned.source),
236    }
237}
238
239// In the summary, the root predicate name is empty
240fn summary_predicate_name(pred_name: &str) -> &str {
241    match pred_name {
242        "" => " (predicate)",
243        _ => pred_name,
244    }
245}
246
247/// Determine the width of the column required to fit the name and all
248/// name+predicate combos.
249fn name_col_w(name: &str, built: &BuiltPkg) -> usize {
250    let mut name_w = 0;
251    if let BuiltPkg::Contract(contract) = built {
252        for predicate in &contract.predicate_metadata {
253            let w = summary_predicate_name(&predicate.name).chars().count();
254            name_w = std::cmp::max(name_w, w);
255        }
256    }
257    name.chars().count() + name_w
258}