Skip to main content

soroban_cli/commands/contract/
build.rs

1use cargo_metadata::{Metadata, MetadataCommand, Package};
2use clap::Parser;
3use itertools::Itertools;
4use rustc_version::version;
5use semver::Version;
6use sha2::{Digest, Sha256};
7use std::{
8    borrow::Cow,
9    collections::HashSet,
10    env,
11    ffi::OsStr,
12    fmt::Debug,
13    fs,
14    io::{self, Cursor},
15    path::{self, Path, PathBuf},
16    process::{Command, ExitStatus, Stdio},
17};
18use stellar_xdr::curr::{Limited, Limits, ScMetaEntry, ScMetaV0, StringM, WriteXdr};
19
20#[cfg(feature = "additional-libs")]
21use crate::commands::contract::optimize;
22use crate::{
23    commands::{global, version},
24    print::Print,
25    wasm,
26};
27
28/// Build a contract from source
29///
30/// Builds all crates that are referenced by the cargo manifest (Cargo.toml)
31/// that have cdylib as their crate-type. Crates are built for the wasm32
32/// target. Unless configured otherwise, crates are built with their default
33/// features and with their release profile.
34///
35/// In workspaces builds all crates unless a package name is specified, or the
36/// command is executed from the sub-directory of a workspace crate.
37///
38/// To view the commands that will be executed, without executing them, use the
39/// --print-commands-only option.
40#[derive(Parser, Debug, Clone)]
41#[allow(clippy::struct_excessive_bools)]
42pub struct Cmd {
43    /// Path to Cargo.toml
44    #[arg(long)]
45    pub manifest_path: Option<std::path::PathBuf>,
46    /// Package to build
47    ///
48    /// If omitted, all packages that build for crate-type cdylib are built.
49    #[arg(long)]
50    pub package: Option<String>,
51
52    /// Build with the specified profile
53    #[arg(long, default_value = "release")]
54    pub profile: String,
55
56    /// Build with the list of features activated, space or comma separated
57    #[arg(long, help_heading = "Features")]
58    pub features: Option<String>,
59
60    /// Build with the all features activated
61    #[arg(
62        long,
63        conflicts_with = "features",
64        conflicts_with = "no_default_features",
65        help_heading = "Features"
66    )]
67    pub all_features: bool,
68
69    /// Build with the default feature not activated
70    #[arg(long, help_heading = "Features")]
71    pub no_default_features: bool,
72
73    /// Directory to copy wasm files to
74    ///
75    /// If provided, wasm files can be found in the cargo target directory, and
76    /// the specified directory.
77    ///
78    /// If ommitted, wasm files are written only to the cargo target directory.
79    #[arg(long)]
80    pub out_dir: Option<std::path::PathBuf>,
81
82    /// Print commands to build without executing them
83    #[arg(long, conflicts_with = "out_dir", help_heading = "Other")]
84    pub print_commands_only: bool,
85
86    /// Add key-value to contract meta (adds the meta to the `contractmetav0` custom section)
87    #[arg(long, num_args=1, value_parser=parse_meta_arg, action=clap::ArgAction::Append, help_heading = "Metadata")]
88    pub meta: Vec<(String, String)>,
89
90    /// Optimize the generated wasm.
91    #[cfg_attr(feature = "additional-libs", arg(long))]
92    #[cfg_attr(not(feature = "additional-libs"), arg(long, hide = true))]
93    pub optimize: bool,
94}
95
96fn parse_meta_arg(s: &str) -> Result<(String, String), Error> {
97    let parts = s.splitn(2, '=');
98
99    let (key, value) = parts
100        .map(str::trim)
101        .next_tuple()
102        .ok_or_else(|| Error::MetaArg("must be in the form 'key=value'".to_string()))?;
103
104    Ok((key.to_string(), value.to_string()))
105}
106
107#[derive(thiserror::Error, Debug)]
108pub enum Error {
109    #[error(transparent)]
110    Metadata(#[from] cargo_metadata::Error),
111
112    #[error(transparent)]
113    CargoCmd(io::Error),
114
115    #[error("exit status {0}")]
116    Exit(ExitStatus),
117
118    #[error("package {package} not found")]
119    PackageNotFound { package: String },
120
121    #[error("finding absolute path of Cargo.toml: {0}")]
122    AbsolutePath(io::Error),
123
124    #[error("creating out directory: {0}")]
125    CreatingOutDir(io::Error),
126
127    #[error("deleting existing artifact: {0}")]
128    DeletingArtifact(io::Error),
129
130    #[error("copying wasm file: {0}")]
131    CopyingWasmFile(io::Error),
132
133    #[error("getting the current directory: {0}")]
134    GettingCurrentDir(io::Error),
135
136    #[error("retrieving CARGO_HOME: {0}")]
137    CargoHome(io::Error),
138
139    #[error("reading wasm file: {0}")]
140    ReadingWasmFile(io::Error),
141
142    #[error("writing wasm file: {0}")]
143    WritingWasmFile(io::Error),
144
145    #[error("invalid meta entry: {0}")]
146    MetaArg(String),
147
148    #[error(
149        "use a rust version other than 1.81, 1.82, 1.83 or 1.91.0 to build contracts (got {0})"
150    )]
151    RustVersion(String),
152
153    #[error("must install with \"additional-libs\" feature.")]
154    OptimizeFeatureNotEnabled,
155
156    #[error("invalid Cargo.toml configuration: {0}")]
157    CargoConfiguration(String),
158
159    #[error(transparent)]
160    Xdr(#[from] stellar_xdr::curr::Error),
161
162    #[cfg(feature = "additional-libs")]
163    #[error(transparent)]
164    Optimize(#[from] optimize::Error),
165
166    #[error(transparent)]
167    Wasm(#[from] wasm::Error),
168}
169
170const WASM_TARGET: &str = "wasm32v1-none";
171const WASM_TARGET_OLD: &str = "wasm32-unknown-unknown";
172const META_CUSTOM_SECTION_NAME: &str = "contractmetav0";
173
174impl Cmd {
175    #[allow(clippy::too_many_lines)]
176    pub fn run(&self, global_args: &global::Args) -> Result<(), Error> {
177        let print = Print::new(global_args.quiet);
178        let working_dir = env::current_dir().map_err(Error::GettingCurrentDir)?;
179        let metadata = self.metadata()?;
180        let packages = self.packages(&metadata)?;
181        let target_dir = &metadata.target_directory;
182
183        // Run build configuration checks (only when actually building)
184        if !self.print_commands_only {
185            run_checks(metadata.workspace_root.as_std_path(), &self.profile)?;
186        }
187
188        if let Some(package) = &self.package {
189            if packages.is_empty() {
190                return Err(Error::PackageNotFound {
191                    package: package.clone(),
192                });
193            }
194        }
195
196        let wasm_target = get_wasm_target()?;
197
198        for p in packages {
199            let mut cmd = Command::new("cargo");
200            cmd.stdout(Stdio::piped());
201            cmd.arg("rustc");
202            let manifest_path = pathdiff::diff_paths(&p.manifest_path, &working_dir)
203                .unwrap_or(p.manifest_path.clone().into());
204            cmd.arg(format!(
205                "--manifest-path={}",
206                manifest_path.to_string_lossy()
207            ));
208            cmd.arg("--crate-type=cdylib");
209            cmd.arg(format!("--target={wasm_target}"));
210            if self.profile == "release" {
211                cmd.arg("--release");
212            } else {
213                cmd.arg(format!("--profile={}", self.profile));
214            }
215            if self.all_features {
216                cmd.arg("--all-features");
217            }
218            if self.no_default_features {
219                cmd.arg("--no-default-features");
220            }
221            if let Some(features) = self.features() {
222                let requested: HashSet<String> = features.iter().cloned().collect();
223                let available = p.features.iter().map(|f| f.0).cloned().collect();
224                let activate = requested.intersection(&available).join(",");
225                if !activate.is_empty() {
226                    cmd.arg(format!("--features={activate}"));
227                }
228            }
229
230            if let Some(rustflags) = make_rustflags_to_remap_absolute_paths(&print)? {
231                cmd.env("CARGO_BUILD_RUSTFLAGS", rustflags);
232            }
233
234            let mut cmd_str_parts = Vec::<String>::new();
235            cmd_str_parts.extend(cmd.get_envs().map(|(key, val)| {
236                format!(
237                    "{}={}",
238                    key.to_string_lossy(),
239                    shell_escape::escape(val.unwrap_or_default().to_string_lossy())
240                )
241            }));
242            cmd_str_parts.push("cargo".to_string());
243            cmd_str_parts.extend(
244                cmd.get_args()
245                    .map(OsStr::to_string_lossy)
246                    .map(Cow::into_owned),
247            );
248            let cmd_str = cmd_str_parts.join(" ");
249
250            if self.print_commands_only {
251                println!("{cmd_str}");
252            } else {
253                print.infoln(cmd_str);
254                let status = cmd.status().map_err(Error::CargoCmd)?;
255                if !status.success() {
256                    return Err(Error::Exit(status));
257                }
258
259                let wasm_name = p.name.replace('-', "_");
260                let file = format!("{wasm_name}.wasm");
261                let target_file_path = Path::new(target_dir)
262                    .join(&wasm_target)
263                    .join(&self.profile)
264                    .join(&file);
265
266                self.inject_meta(&target_file_path)?;
267
268                let final_path = if let Some(out_dir) = &self.out_dir {
269                    fs::create_dir_all(out_dir).map_err(Error::CreatingOutDir)?;
270                    let out_file_path = Path::new(out_dir).join(&file);
271                    fs::copy(target_file_path, &out_file_path).map_err(Error::CopyingWasmFile)?;
272                    out_file_path
273                } else {
274                    target_file_path
275                };
276
277                let wasm_bytes = fs::read(&final_path).map_err(Error::ReadingWasmFile)?;
278                #[cfg_attr(not(feature = "additional-libs"), allow(unused_mut))]
279                let mut optimized_wasm_bytes: Vec<u8> = Vec::new();
280
281                #[cfg(feature = "additional-libs")]
282                if self.optimize {
283                    let mut path = final_path.clone();
284                    path.set_extension("optimized.wasm");
285                    optimize::optimize(true, vec![final_path.clone()], Some(path.clone()))?;
286                    optimized_wasm_bytes = fs::read(&path).map_err(Error::ReadingWasmFile)?;
287
288                    fs::remove_file(&final_path).map_err(Error::DeletingArtifact)?;
289                    fs::rename(&path, &final_path).map_err(Error::CopyingWasmFile)?;
290                }
291
292                #[cfg(not(feature = "additional-libs"))]
293                if self.optimize {
294                    return Err(Error::OptimizeFeatureNotEnabled);
295                }
296
297                Self::print_build_summary(&print, &final_path, wasm_bytes, optimized_wasm_bytes);
298            }
299        }
300
301        Ok(())
302    }
303
304    fn features(&self) -> Option<Vec<String>> {
305        self.features
306            .as_ref()
307            .map(|f| f.split(&[',', ' ']).map(String::from).collect())
308    }
309
310    fn packages(&self, metadata: &Metadata) -> Result<Vec<Package>, Error> {
311        // Filter by the package name if one is provided, or by the package that
312        // matches the manifest path if the manifest path matches a specific
313        // package.
314        let name = if let Some(name) = self.package.clone() {
315            Some(name)
316        } else {
317            // When matching a package based on the manifest path, match against the
318            // absolute path because the paths in the metadata are absolute. Match
319            // against a manifest in the current working directory if no manifest is
320            // specified.
321            let manifest_path = path::absolute(
322                self.manifest_path
323                    .clone()
324                    .unwrap_or(PathBuf::from("Cargo.toml")),
325            )
326            .map_err(Error::AbsolutePath)?;
327            metadata
328                .packages
329                .iter()
330                .find(|p| p.manifest_path == manifest_path)
331                .map(|p| p.name.clone())
332        };
333
334        let packages = metadata
335            .packages
336            .iter()
337            .filter(|p|
338                // Filter by the package name if one is selected based on the above logic.
339                if let Some(name) = &name {
340                    &p.name == name
341                } else {
342                    // Otherwise filter crates that are default members of the
343                    // workspace and that build to cdylib (wasm).
344                    metadata.workspace_default_members.contains(&p.id)
345                        && p.targets
346                            .iter()
347                            .any(|t| t.crate_types.iter().any(|c| c == "cdylib"))
348                }
349            )
350            .cloned()
351            .collect();
352
353        Ok(packages)
354    }
355
356    fn metadata(&self) -> Result<Metadata, cargo_metadata::Error> {
357        let mut cmd = MetadataCommand::new();
358        cmd.no_deps();
359        // Set the manifest path if one is provided, otherwise rely on the cargo
360        // commands default behavior of finding the nearest Cargo.toml in the
361        // current directory, or the parent directories above it.
362        if let Some(manifest_path) = &self.manifest_path {
363            cmd.manifest_path(manifest_path);
364        }
365        // Do not configure features on the metadata command, because we are
366        // only collecting non-dependency metadata, features have no impact on
367        // the output.
368        cmd.exec()
369    }
370
371    fn inject_meta(&self, target_file_path: &PathBuf) -> Result<(), Error> {
372        let mut wasm_bytes = fs::read(target_file_path).map_err(Error::ReadingWasmFile)?;
373        let xdr = self.encoded_new_meta()?;
374        wasm_gen::write_custom_section(&mut wasm_bytes, META_CUSTOM_SECTION_NAME, &xdr);
375
376        // Deleting .wasm file effectively unlinking it from /release/deps/.wasm preventing from overwrite
377        // See https://github.com/stellar/stellar-cli/issues/1694#issuecomment-2709342205
378        fs::remove_file(target_file_path).map_err(Error::DeletingArtifact)?;
379        fs::write(target_file_path, wasm_bytes).map_err(Error::WritingWasmFile)
380    }
381
382    fn encoded_new_meta(&self) -> Result<Vec<u8>, Error> {
383        let mut new_meta: Vec<ScMetaEntry> = Vec::new();
384
385        // Always inject CLI version
386        let cli_meta_entry = ScMetaEntry::ScMetaV0(ScMetaV0 {
387            key: "cliver".to_string().try_into().unwrap(),
388            val: version::one_line().clone().try_into().unwrap(),
389        });
390        new_meta.push(cli_meta_entry);
391
392        // Add args provided meta
393        for (k, v) in self.meta.clone() {
394            let key: StringM = k
395                .clone()
396                .try_into()
397                .map_err(|e| Error::MetaArg(format!("{k} is an invalid metadata key: {e}")))?;
398
399            let val: StringM = v
400                .clone()
401                .try_into()
402                .map_err(|e| Error::MetaArg(format!("{v} is an invalid metadata value: {e}")))?;
403            let meta_entry = ScMetaEntry::ScMetaV0(ScMetaV0 { key, val });
404            new_meta.push(meta_entry);
405        }
406
407        let mut buffer = Vec::new();
408        let mut writer = Limited::new(Cursor::new(&mut buffer), Limits::none());
409        for entry in new_meta {
410            entry.write_xdr(&mut writer)?;
411        }
412        Ok(buffer)
413    }
414
415    fn print_build_summary(
416        print: &Print,
417        path: &Path,
418        wasm_bytes: Vec<u8>,
419        optimized_wasm_bytes: Vec<u8>,
420    ) {
421        print.infoln("Build Summary:");
422
423        let rel_path = path
424            .strip_prefix(env::current_dir().unwrap())
425            .unwrap_or(path);
426
427        let size = wasm_bytes.len();
428        let optimized_size = optimized_wasm_bytes.len();
429
430        let size_description = if optimized_size > 0 {
431            format!("{optimized_size} bytes optimized (original size was {size} bytes)")
432        } else {
433            format!("{size} bytes")
434        };
435
436        let bytes = if optimized_size > 0 {
437            &optimized_wasm_bytes
438        } else {
439            &wasm_bytes
440        };
441
442        print.blankln(format!(
443            "Wasm File: {path} ({size_description})",
444            path = rel_path.display()
445        ));
446
447        print.blankln(format!("Wasm Hash: {}", hex::encode(Sha256::digest(bytes))));
448        print.blankln(format!("Wasm Size: {size_description}"));
449
450        let parser = wasmparser::Parser::new(0);
451        let export_names: Vec<&str> = parser
452            .parse_all(&wasm_bytes)
453            .filter_map(Result::ok)
454            .filter_map(|payload| {
455                if let wasmparser::Payload::ExportSection(exports) = payload {
456                    Some(exports)
457                } else {
458                    None
459                }
460            })
461            .flatten()
462            .filter_map(Result::ok)
463            .filter(|export| matches!(export.kind, wasmparser::ExternalKind::Func))
464            .map(|export| export.name)
465            .sorted()
466            .collect();
467
468        if export_names.is_empty() {
469            print.blankln("Exported Functions: None found");
470        } else {
471            print.blankln(format!("Exported Functions: {} found", export_names.len()));
472            for name in export_names {
473                print.blankln(format!("  • {name}"));
474            }
475        }
476
477        print.checkln("Build Complete\n");
478    }
479}
480
481/// Configure cargo/rustc to replace absolute paths in panic messages / debuginfo
482/// with relative paths.
483///
484/// This is required for reproducible builds.
485///
486/// This works for paths to crates in the registry. The compiler already does
487/// something similar for standard library paths and local paths. It may not
488/// work for crates that come from other sources, including the standard library
489/// compiled from source, though it may be possible to accomodate such cases in
490/// the future.
491///
492/// This in theory breaks the ability of debuggers to find source code, but
493/// since we are only targetting wasm, which is not typically run in a debugger,
494/// and stellar-cli only compiles contracts in release mode, the impact is on
495/// debugging is expected to be minimal.
496///
497/// This works by setting the `CARGO_BUILD_RUSTFLAGS` environment variable,
498/// with appropriate `--remap-path-prefix` option. It preserves the values of an
499/// existing `CARGO_BUILD_RUSTFLAGS` environment variable.
500///
501/// This must be done some via some variation of `RUSTFLAGS` and not as
502/// arguments to `cargo rustc` because the latter only applies to the crate
503/// directly being compiled, while `RUSTFLAGS` applies to all crates, including
504/// dependencies.
505///
506/// `CARGO_BUILD_RUSTFLAGS` is an alias for the `build.rustflags` configuration
507/// variable. Cargo automatically merges the contents of the environment variable
508/// and the variables from config files; and `build.rustflags` has the lowest
509/// priority of all the variations of rustflags that Cargo accepts. And because
510/// we merge our values with an existing `CARGO_BUILD_RUSTFLAGS`,
511/// our setting of this environment variable should not interfere with the
512/// user's ability to set rustflags in any way they want, but it does mean
513/// that if the user sets a higher-priority rustflags that our path remapping
514/// will be ignored.
515///
516/// The major downside of using `CARGO_BUILD_RUSTFLAGS` is that it is whitespace
517/// separated, which means we cannot support paths with spaces. If we encounter
518/// such paths we will emit a warning. Spaces could be accomodated by using
519/// `CARGO_ENCODED_RUSTFLAGS`, but that has high precedence over other rustflags,
520/// so we could be interfering with the user's own use of rustflags. There is
521/// no "encoded" variant of `CARGO_BUILD_RUSTFLAGS` at time of writing.
522///
523/// This assumes that paths are Unicode and that any existing `CARGO_BUILD_RUSTFLAGS`
524/// variables are Unicode. Non-Unicode paths will fail to correctly perform the
525/// the absolute path replacement. Non-Unicode `CARGO_BUILD_RUSTFLAGS` will result in the
526/// existing rustflags being ignored, which is also the behavior of
527/// Cargo itself.
528fn make_rustflags_to_remap_absolute_paths(print: &Print) -> Result<Option<String>, Error> {
529    let cargo_home = home::cargo_home().map_err(Error::CargoHome)?;
530
531    if format!("{}", cargo_home.display())
532        .find(|c: char| c.is_whitespace())
533        .is_some()
534    {
535        print.warnln("Cargo home directory contains whitespace. Dependency paths will not be remapped; builds may not be reproducible.");
536        return Ok(None);
537    }
538
539    if env::var("RUSTFLAGS").is_ok() {
540        print.warnln("`RUSTFLAGS` set. Dependency paths will not be remapped; builds may not be reproducible. Use CARGO_BUILD_RUSTFLAGS instead, which the CLI will merge with remapping.");
541        return Ok(None);
542    }
543
544    if env::var("CARGO_ENCODED_RUSTFLAGS").is_ok() {
545        print.warnln("`CARGO_ENCODED_RUSTFLAGS` set. Dependency paths will not be remapped; builds may not be reproducible.");
546        return Ok(None);
547    }
548
549    let target = get_wasm_target()?;
550    let env_var_name = format!("TARGET_{target}_RUSTFLAGS");
551
552    if env::var(env_var_name.clone()).is_ok() {
553        print.warnln(format!("`{env_var_name}` set. Dependency paths will not be remapped; builds may not be reproducible."));
554        return Ok(None);
555    }
556
557    let registry_prefix = cargo_home.join("registry").join("src");
558    let registry_prefix_str = registry_prefix.display().to_string();
559    #[cfg(windows)]
560    let registry_prefix_str = registry_prefix_str.replace('\\', "/");
561    let new_rustflag = format!("--remap-path-prefix={registry_prefix_str}=");
562
563    let mut rustflags = get_rustflags().unwrap_or_default();
564    rustflags.push(new_rustflag);
565
566    let rustflags = rustflags.join(" ");
567
568    Ok(Some(rustflags))
569}
570
571/// Get any existing `CARGO_BUILD_RUSTFLAGS`, split on whitespace.
572///
573/// This conveniently ignores non-Unicode values, as does Cargo.
574fn get_rustflags() -> Option<Vec<String>> {
575    if let Ok(a) = env::var("CARGO_BUILD_RUSTFLAGS") {
576        let args = a
577            .split_whitespace()
578            .map(str::trim)
579            .filter(|s| !s.is_empty())
580            .map(str::to_string);
581        return Some(args.collect());
582    }
583
584    None
585}
586
587fn get_wasm_target() -> Result<String, Error> {
588    let Ok(current_version) = version() else {
589        return Ok(WASM_TARGET.into());
590    };
591
592    let v184 = Version::parse("1.84.0").unwrap();
593    let v182 = Version::parse("1.82.0").unwrap();
594    let v191 = Version::parse("1.91.0").unwrap();
595
596    if current_version == v191 {
597        return Err(Error::RustVersion(current_version.to_string()));
598    }
599
600    if current_version >= v182 && current_version < v184 {
601        return Err(Error::RustVersion(current_version.to_string()));
602    }
603
604    if current_version < v184 {
605        Ok(WASM_TARGET_OLD.into())
606    } else {
607        Ok(WASM_TARGET.into())
608    }
609}
610
611/// Run build configuration checks and return an error if configuration is invalid.
612fn run_checks(workspace_root: &Path, profile: &str) -> Result<(), Error> {
613    let cargo_toml_path = workspace_root.join("Cargo.toml");
614
615    let cargo_toml_str = match fs::read_to_string(&cargo_toml_path) {
616        Ok(s) => s,
617        Err(e) => {
618            return Err(Error::CargoConfiguration(format!(
619                "Could not read Cargo.toml: {e}"
620            )));
621        }
622    };
623
624    let doc: toml_edit::DocumentMut = match cargo_toml_str.parse() {
625        Ok(d) => d,
626        Err(e) => {
627            return Err(Error::CargoConfiguration(format!(
628                "Could not parse Cargo.toml to run checks: {e}"
629            )));
630        }
631    };
632
633    check_overflow_checks(&doc, profile)?;
634    // Future checks can be added here
635    Ok(())
636}
637
638/// Check if overflow-checks is enabled for the specified profile.
639/// Returns an error if not enabled.
640fn check_overflow_checks(doc: &toml_edit::DocumentMut, profile: &str) -> Result<(), Error> {
641    // Helper to check a profile and follow inheritance chain
642    // Returns Some(bool) if overflow-checks is found, None if not found
643    fn get_overflow_checks(
644        doc: &toml_edit::DocumentMut,
645        profile: &str,
646        visited: &mut Vec<String>,
647    ) -> Option<bool> {
648        if visited.contains(&profile.to_string()) {
649            return None; // Prevent infinite loops
650        }
651        visited.push(profile.to_string());
652
653        let profile_section = doc.get("profile")?.get(profile)?;
654
655        // Check if overflow-checks is explicitly set
656        if let Some(val) = profile_section
657            .get("overflow-checks")
658            .and_then(toml_edit::Item::as_bool)
659        {
660            return Some(val);
661        }
662
663        // Check inherited profile
664        if let Some(inherits) = profile_section.get("inherits").and_then(|v| v.as_str()) {
665            return get_overflow_checks(doc, inherits, visited);
666        }
667
668        None
669    }
670
671    let mut visited = Vec::new();
672    if get_overflow_checks(doc, profile, &mut visited) == Some(true) {
673        Ok(())
674    } else {
675        Err(Error::CargoConfiguration(format!(
676            "`overflow-checks` is not enabled for profile `{profile}`. \
677            To prevent silent integer overflow, add `overflow-checks = true` to \
678            [profile.{profile}] in your Cargo.toml."
679        )))
680    }
681}