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