scrypto_compiler/
lib.rs

1use cargo_toml::Manifest;
2use fslock::{LockFile, ToOsStr};
3use radix_common::prelude::*;
4use radix_engine::utils::{extract_definition, ExtractSchemaError};
5use radix_engine_interface::{blueprints::package::PackageDefinition, types::Level};
6use radix_rust::prelude::{IndexMap, IndexSet};
7use std::cmp::Ordering;
8use std::error::Error;
9use std::iter;
10use std::path::{Path, PathBuf};
11use std::process::{Command, ExitStatus, Stdio};
12use std::{env, io};
13
14const MANIFEST_FILE: &str = "Cargo.toml";
15const BUILD_TARGET: &str = "wasm32-unknown-unknown";
16const SCRYPTO_NO_SCHEMA: &str = "scrypto/no-schema";
17const SCRYPTO_COVERAGE: &str = "scrypto/coverage";
18
19// Radix Engine supports WASM MVP + proposals: mmutable-globals and sign-extension-ops
20// (see radix-engine/src/vm/wasm.prepare.rs)
21// More on CFLAGS for WASM:  https://clang.llvm.org/docs/ClangCommandLineReference.html#webassembly
22const TARGET_CLAGS_FOR_WASM: &str = "-mcpu=mvp -mmutable-globals -msign-ext";
23
24#[derive(Debug)]
25pub enum ScryptoCompilerError {
26    /// Returns IO Error which occurred during compilation and optional context information.
27    IOError(io::Error, Option<String>),
28    /// Returns IO Error which occurred during compilation, path of a file related to that fail and
29    /// optional context information.
30    IOErrorWithPath(io::Error, PathBuf, Option<String>),
31    /// Returns process exit status in case of 'cargo build' fail.
32    CargoBuildFailure(ExitStatus),
33    /// Returns `cargo metadata` command stderr output, path to Cargo.toml for which cargo metadata
34    /// command failed and process exit status.
35    CargoMetadataFailure(String, PathBuf, ExitStatus),
36    /// Returns path to Cargo.toml for which results of cargo metadata command is not not valid json
37    /// or target directory field is missing.
38    CargoTargetDirectoryResolutionError(String),
39    /// Compiler is unable to generate target binary file name.
40    CargoTargetBinaryResolutionError,
41    /// Returns path to Cargo.toml which was failed to load.
42    CargoManifestLoadFailure(String),
43    /// Returns path to Cargo.toml which cannot be found.
44    CargoManifestFileNotFound(String),
45    /// Provided package ID is not a member of the workspace.
46    CargoWrongPackageId(String),
47    /// Returns WASM Optimization error.
48    WasmOptimizationError(wasm_opt::OptimizationError),
49    /// Returns error occured during schema extraction.
50    SchemaExtractionError(ExtractSchemaError),
51    /// Returns error occured during schema encoding.
52    SchemaEncodeError(EncodeError),
53    /// Returns error occured during schema decoding.
54    SchemaDecodeError(DecodeError),
55    /// Returned when trying to compile workspace without any scrypto packages.
56    NothingToCompile,
57}
58
59#[derive(Debug, Clone)]
60pub struct ScryptoCompilerInputParams {
61    /// Path to Cargo.toml file, if not specified current directory will be used.
62    pub manifest_path: Option<PathBuf>,
63    /// Path to directory where compilation artifacts are stored, if not specified default location will by used.
64    pub target_directory: Option<PathBuf>,
65    /// Compilation profile. If not specified default profile: Release will be used.
66    pub profile: Profile,
67    /// List of environment variables to set or unset during compilation.
68    /// By default it includes compilation flags for C libraries to configure WASM with the same
69    /// features as Radix Engine.
70    /// TARGET_CFLAGS="-mcpu=mvp -mmutable-globals -msign-ext"
71    pub environment_variables: IndexMap<String, EnvironmentVariableAction>,
72    /// List of features, used for 'cargo build --features'. Optional field.
73    pub features: IndexSet<String>,
74    /// If set to true then '--no-default-features' option is passed to 'cargo build'. The default value is false.
75    pub no_default_features: bool,
76    /// If set to true then '--all-features' option is passed to 'cargo build'. The default value is false.
77    pub all_features: bool,
78    /// List of packages to compile, used for 'cargo build --package'. Optional field.
79    pub package: IndexSet<String>,
80    /// If set to true then '--locked' option is passed to 'cargo build', which enforces using the `Cargo.lock` file without changes. The default value is false.
81    pub locked: bool,
82    /// If set, the `SCRYPTO_CARGO_LOCKED` environment variable is ignored.
83    /// This is useful for unit tests in this repo, which need to run successfully independent of this setting.
84    /// Defaults to false.
85    pub ignore_locked_env_var: bool,
86    /// List of custom options, passed as 'cargo build' arguments without any modifications. Optional field.
87    /// Add each option as separate entry (for instance: '-j 1' must be added as two entires: '-j' and '1' one by one).
88    pub custom_options: IndexSet<String>,
89    /// If specified optimizes the built wasm using Binaryen's wasm-opt tool.
90    /// Default configuration is equivalent to running the following commands in the CLI:
91    /// wasm-opt -0z --strip-debug --strip-dwarf --strip-producers --dce $some_path $some_path
92    pub wasm_optimization: Option<wasm_opt::OptimizationOptions>,
93    /// If set to true then compiler informs about the compilation progress
94    pub verbose: bool,
95}
96impl Default for ScryptoCompilerInputParams {
97    /// Definition of default `ScryptoCompiler` configuration.
98    fn default() -> Self {
99        let wasm_optimization = Some(
100            wasm_opt::OptimizationOptions::new_optimize_for_size_aggressively()
101                .add_pass(wasm_opt::Pass::StripDebug)
102                .add_pass(wasm_opt::Pass::StripDwarf)
103                .add_pass(wasm_opt::Pass::StripProducers)
104                .add_pass(wasm_opt::Pass::Dce)
105                .to_owned(),
106        );
107        let mut ret = Self {
108            manifest_path: None,
109            target_directory: None,
110            profile: Profile::Release,
111            environment_variables: indexmap!(
112                "TARGET_CFLAGS".to_string() =>
113                EnvironmentVariableAction::Set(
114                    TARGET_CLAGS_FOR_WASM.to_string()
115                )
116            ),
117            features: indexset!(),
118            no_default_features: false,
119            all_features: false,
120            package: indexset!(),
121            custom_options: indexset!(),
122            ignore_locked_env_var: false,
123            locked: false,
124            wasm_optimization,
125            verbose: false,
126        };
127        // Apply default log level features
128        ret.features
129            .extend(Self::log_level_to_scrypto_features(Level::default()).into_iter());
130        ret
131    }
132}
133impl ScryptoCompilerInputParams {
134    pub fn log_level_to_scrypto_features(log_level: Level) -> Vec<String> {
135        let mut ret = Vec::new();
136        if Level::Error <= log_level {
137            ret.push(String::from("scrypto/log-error"));
138        }
139        if Level::Warn <= log_level {
140            ret.push(String::from("scrypto/log-warn"));
141        }
142        if Level::Info <= log_level {
143            ret.push(String::from("scrypto/log-info"));
144        }
145        if Level::Debug <= log_level {
146            ret.push(String::from("scrypto/log-debug"));
147        }
148        if Level::Trace <= log_level {
149            ret.push(String::from("scrypto/log-trace"));
150        }
151        ret
152    }
153}
154
155#[derive(Debug, Default, Clone)]
156pub enum Profile {
157    #[default]
158    Release,
159    Debug,
160    Test,
161    Bench,
162    Custom(String),
163}
164impl Profile {
165    fn as_command_args(&self) -> Vec<String> {
166        vec![
167            String::from("--profile"),
168            match self {
169                Profile::Release => String::from("release"),
170                Profile::Debug => String::from("dev"),
171                Profile::Test => String::from("test"),
172                Profile::Bench => String::from("bench"),
173                Profile::Custom(name) => name.clone(),
174            },
175        ]
176    }
177    fn as_target_directory_name(&self) -> String {
178        match self {
179            Profile::Release => String::from("release"),
180            Profile::Debug => String::from("debug"),
181            Profile::Test => String::from("debug"),
182            Profile::Bench => String::from("release"),
183            Profile::Custom(name) => name.clone(),
184        }
185    }
186}
187#[derive(Debug, PartialEq, Eq)]
188pub struct ParseProfileError;
189impl fmt::Display for ParseProfileError {
190    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191        f.write_fmt(format_args!("{:?}", self))
192    }
193}
194impl Error for ParseProfileError {}
195
196impl FromStr for Profile {
197    type Err = ParseProfileError;
198
199    fn from_str(s: &str) -> Result<Self, Self::Err> {
200        match s {
201            "release" => Ok(Profile::Release),
202            "debug" => Ok(Profile::Debug),
203            "test" => Ok(Profile::Test),
204            "bench" => Ok(Profile::Bench),
205            other => {
206                if other.contains(' ') {
207                    Err(ParseProfileError)
208                } else {
209                    Ok(Profile::Custom(other.to_string()))
210                }
211            }
212        }
213    }
214}
215
216#[derive(Debug, Clone)]
217pub enum EnvironmentVariableAction {
218    Set(String),
219    Unset,
220}
221
222#[derive(Debug, Clone)]
223pub struct BuildArtifacts {
224    pub wasm: BuildArtifact<Vec<u8>>,
225    pub package_definition: BuildArtifact<PackageDefinition>,
226}
227
228#[derive(Debug, Clone)]
229pub struct BuildArtifact<T> {
230    pub path: PathBuf,
231    pub content: T,
232}
233
234#[derive(Debug, Clone)]
235pub struct CompilerManifestDefinition {
236    /// Path to Cargo.toml file.
237    pub manifest_path: PathBuf,
238    /// Path to directory where compilation artifacts are stored.
239    pub target_directory: PathBuf,
240    /// Target binary name
241    pub target_binary_name: String,
242    /// Path to target binary WASM file from phase 1.
243    pub target_phase_1_build_wasm_output_path: PathBuf,
244    /// Path to target binary WASM file from phase 2.
245    pub target_phase_2_build_wasm_output_path: PathBuf,
246    /// Path to target binary RPD file.
247    pub target_output_binary_rpd_path: PathBuf,
248    /// Path to target binary WASM file with schema.
249    pub target_copied_wasm_with_schema_path: PathBuf,
250}
251
252// Helper enum to unify different iterator types
253enum Either<L, R> {
254    Left(L),
255    Right(R),
256}
257
258impl<L, R> Iterator for Either<L, R>
259where
260    L: Iterator,
261    R: Iterator<Item = L::Item>,
262{
263    type Item = L::Item;
264
265    fn next(&mut self) -> Option<Self::Item> {
266        match self {
267            Either::Left(iter) => iter.next(),
268            Either::Right(iter) => iter.next(),
269        }
270    }
271}
272
273/// Programmatic implementation of Scrypto compiler which is a wrapper around rust cargo tool.
274/// To create an instance of `ScryptoCompiler` use `builder()` constructor which implements builder pattern,
275/// provide any required parameter @see `ScryptoCompilerInputParams` and finally call `compile()` function.
276/// `ScryptoCompiler` supports worspace compilation by providing workspace manifest as `manifest_path` parameter of
277/// running compiler from directory containg workspace Cargo.toml file. Only packages with defined metadata group:
278/// [package.metadata.scrypto] will be used during workspace compilation (so workspace manifest can contain also non
279/// Scrypto packages). Alternativelly packages for workspace compilation can be provided in `package` input parameter,
280/// metadata is not validated in that case.
281/// Compilation results consists of list of `BuildArtifacts` which contains generated WASM file path and its content
282/// and path to RPD file with package definition and `PackageDefinition` struct.
283pub struct ScryptoCompiler {
284    /// Scrypto compiler input parameters.
285    input_params: ScryptoCompilerInputParams,
286    /// Manifest definition used in 'cargo build' command calls. For workspace compilation this is a workspace manifest,
287    /// for non-workspace compilation it is particular project manifest.
288    /// 'cargo build' command will automatically build all workspace members for workspace compilation.
289    main_manifest: CompilerManifestDefinition,
290    /// List of manifest definitions in workspace compilation.
291    manifests: Vec<CompilerManifestDefinition>,
292}
293
294#[derive(Debug)]
295struct PackageLock {
296    pub path: PathBuf,
297    pub lock: LockFile,
298}
299
300impl PartialEq for PackageLock {
301    fn eq(&self, other: &Self) -> bool {
302        self.path == other.path
303    }
304}
305impl Eq for PackageLock {}
306
307impl Ord for PackageLock {
308    fn cmp(&self, other: &Self) -> Ordering {
309        self.path.cmp(&other.path)
310    }
311}
312
313impl PartialOrd for PackageLock {
314    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
315        Some(self.cmp(other))
316    }
317}
318
319impl PackageLock {
320    fn new(path: PathBuf) -> Result<Self, ScryptoCompilerError> {
321        let os_path = path.to_os_str().map_err(|err| {
322            ScryptoCompilerError::IOErrorWithPath(
323                err,
324                path.clone(),
325                Some(String::from("Convert lock file path to &str failed")),
326            )
327        })?;
328
329        let lock = LockFile::open(&os_path).map_err(|err| {
330            ScryptoCompilerError::IOErrorWithPath(
331                err,
332                path.clone(),
333                Some(String::from("Open file for locking failed")),
334            )
335        })?;
336
337        Ok(Self { path, lock })
338    }
339
340    fn is_locked(&self) -> bool {
341        self.lock.owns_lock()
342    }
343
344    fn try_lock(&mut self) -> Result<bool, ScryptoCompilerError> {
345        self.lock.try_lock().map_err(|err| {
346            ScryptoCompilerError::IOErrorWithPath(
347                err,
348                self.path.clone(),
349                Some(String::from("Lock file failed")),
350            )
351        })
352    }
353
354    fn unlock(&mut self) -> Result<(), ScryptoCompilerError> {
355        self.lock.unlock().map_err(|err| {
356            ScryptoCompilerError::IOErrorWithPath(
357                err,
358                self.path.clone(),
359                Some(String::from("Unlock file failed")),
360            )
361        })
362    }
363}
364
365impl ScryptoCompiler {
366    pub fn builder() -> ScryptoCompilerBuilder {
367        ScryptoCompilerBuilder::default()
368    }
369
370    // Internal constructor
371    fn from_input_params(
372        input_params: &mut ScryptoCompilerInputParams,
373    ) -> Result<Self, ScryptoCompilerError> {
374        let manifest_path = Self::get_manifest_path(&input_params.manifest_path)?;
375
376        // If compiling workspace use only packages which defines [package.metadata.scrypto]
377        // or only specified packages with --package parameter
378        if let Some(workspace_members) = ScryptoCompiler::is_manifest_workspace(&manifest_path)? {
379            // Verify if provided package names belongs to this workspace
380            if !input_params.package.is_empty() {
381                let wrong_packages: Vec<_> = input_params
382                    .package
383                    .iter()
384                    .filter(|package| {
385                        workspace_members
386                            .iter()
387                            .find(|(_, member_package_name, _)| &member_package_name == package)
388                            .is_none()
389                    })
390                    .collect();
391                if let Some(package) = wrong_packages.first() {
392                    return Err(ScryptoCompilerError::CargoWrongPackageId(
393                        package.to_string(),
394                    ));
395                }
396            } else {
397                input_params.package = workspace_members
398                    .iter()
399                    .filter_map(|(_, package, scrypto_metadata)| {
400                        if scrypto_metadata.is_some() {
401                            Some(package.clone())
402                        } else {
403                            None
404                        }
405                    })
406                    .collect();
407                if input_params.package.is_empty() {
408                    return Err(ScryptoCompilerError::NothingToCompile);
409                }
410            }
411
412            let manifests = workspace_members
413                .into_iter()
414                .filter_map(|(member_manifest_input_path, package, _)| {
415                    if input_params.package.contains(&package) {
416                        Some(
417                            match ScryptoCompiler::get_manifest_path(&Some(
418                                member_manifest_input_path,
419                            )) {
420                                Ok(member_manifest_path) => ScryptoCompiler::prepare_manifest_def(
421                                    input_params,
422                                    &member_manifest_path,
423                                ),
424                                Err(x) => Err(x),
425                            },
426                        )
427                    } else {
428                        None
429                    }
430                })
431                .collect::<Result<Vec<CompilerManifestDefinition>, ScryptoCompilerError>>()?;
432
433            Ok(Self {
434                input_params: input_params.to_owned(),
435                main_manifest: ScryptoCompiler::prepare_manifest_def(input_params, &manifest_path)?,
436                manifests,
437            })
438        } else {
439            Ok(Self {
440                input_params: input_params.to_owned(),
441                main_manifest: ScryptoCompiler::prepare_manifest_def(input_params, &manifest_path)?,
442                manifests: Vec::new(),
443            })
444        }
445    }
446
447    // Generates target paths basing on manifest path
448    fn prepare_manifest_def(
449        input_params: &ScryptoCompilerInputParams,
450        manifest_path: &Path,
451    ) -> Result<CompilerManifestDefinition, ScryptoCompilerError> {
452        ScryptoCompiler::prepare_paths_for_manifest(input_params, manifest_path)
453    }
454
455    fn get_default_target_directory(manifest_path: &Path) -> Result<String, ScryptoCompilerError> {
456        let output = Command::new("cargo")
457            .arg("metadata")
458            .arg("--manifest-path")
459            .arg(manifest_path)
460            .arg("--format-version")
461            .arg("1")
462            .arg("--no-deps")
463            .output()
464            .map_err(|e| {
465                ScryptoCompilerError::IOErrorWithPath(
466                    e,
467                    manifest_path.to_path_buf(),
468                    Some(String::from("Cargo metadata for manifest failed.")),
469                )
470            })?;
471        if output.status.success() {
472            let parsed =
473                serde_json::from_slice::<serde_json::Value>(&output.stdout).map_err(|_| {
474                    ScryptoCompilerError::CargoTargetDirectoryResolutionError(
475                        manifest_path.display().to_string(),
476                    )
477                })?;
478            let target_directory = parsed
479                .as_object()
480                .and_then(|o| o.get("target_directory"))
481                .and_then(|o| o.as_str())
482                .ok_or(ScryptoCompilerError::CargoTargetDirectoryResolutionError(
483                    manifest_path.display().to_string(),
484                ))?;
485            Ok(target_directory.to_owned())
486        } else {
487            Err(ScryptoCompilerError::CargoMetadataFailure(
488                String::from_utf8_lossy(&output.stderr).to_string(),
489                manifest_path.to_path_buf(),
490                output.status,
491            ))
492        }
493    }
494
495    // Returns path to Cargo.toml (including the file)
496    fn get_manifest_path(
497        input_manifest_path: &Option<PathBuf>,
498    ) -> Result<PathBuf, ScryptoCompilerError> {
499        let manifest_path = match input_manifest_path.clone() {
500            Some(mut path) => {
501                if !path.ends_with(MANIFEST_FILE) {
502                    path.push(MANIFEST_FILE);
503                }
504                path
505            }
506            None => {
507                let mut path = env::current_dir().map_err(|e| {
508                    ScryptoCompilerError::IOError(
509                        e,
510                        Some(String::from("Getting current directory failed.")),
511                    )
512                })?;
513                path.push(MANIFEST_FILE);
514                path
515            }
516        };
517
518        if !manifest_path.exists() {
519            Err(ScryptoCompilerError::CargoManifestFileNotFound(
520                manifest_path.display().to_string(),
521            ))
522        } else {
523            Ok(manifest_path)
524        }
525    }
526
527    // If manifest is a workspace this function returns non-empty vector of tuple with workspace members (path),
528    // package name and package scrypto metadata (content of section from Cargo.toml [package.metadata.scrypto]).
529    fn is_manifest_workspace(
530        manifest_path: &Path,
531    ) -> Result<Option<Vec<(PathBuf, String, Option<cargo_toml::Value>)>>, ScryptoCompilerError>
532    {
533        let manifest = Manifest::from_path(&manifest_path).map_err(|_| {
534            ScryptoCompilerError::CargoManifestLoadFailure(manifest_path.display().to_string())
535        })?;
536        if let Some(workspace) = manifest.workspace {
537            if workspace.members.is_empty() {
538                Ok(None)
539            } else {
540                Ok(Some(
541                    workspace
542                        .members
543                        .iter()
544                        .map(|i| {
545                            let mut member_manifest_input_path = manifest_path.to_path_buf();
546                            member_manifest_input_path.pop(); // Workspace Cargo.toml file
547                            member_manifest_input_path.push(PathBuf::from(i));
548                            member_manifest_input_path.push(MANIFEST_FILE); // Manifest Cargo.toml file
549
550                            match Manifest::from_path(&member_manifest_input_path) {
551                                Ok(manifest) => {
552                                    let metadata = match &manifest.package().metadata {
553                                        Some(cargo_toml::Value::Table(map)) => {
554                                            map.get("scrypto").cloned()
555                                        }
556                                        _ => None,
557                                    };
558                                    Ok((
559                                        member_manifest_input_path,
560                                        manifest.package().name().to_string(),
561                                        metadata,
562                                    ))
563                                }
564                                Err(_) => Err(ScryptoCompilerError::CargoManifestLoadFailure(
565                                    member_manifest_input_path.display().to_string(),
566                                )),
567                            }
568                        })
569                        .collect::<Result<Vec<_>, ScryptoCompilerError>>()?,
570                ))
571            }
572        } else {
573            Ok(None)
574        }
575    }
576
577    fn get_target_binary_name(
578        manifest_path: &Path,
579    ) -> Result<Option<String>, ScryptoCompilerError> {
580        // Find the binary name
581        let manifest = Manifest::from_path(&manifest_path).map_err(|_| {
582            ScryptoCompilerError::CargoManifestLoadFailure(manifest_path.display().to_string())
583        })?;
584        if let Some(w) = manifest.workspace {
585            if !w.members.is_empty() {
586                // For workspace compilation there is no binary file for the main manifest
587                return Ok(None);
588            }
589        }
590        let mut wasm_name = None;
591        if let Some(lib) = manifest.lib {
592            wasm_name = lib.name.clone();
593        }
594        if wasm_name.is_none() {
595            if let Some(pkg) = manifest.package {
596                wasm_name = Some(pkg.name.replace("-", "_"));
597            }
598        }
599        Ok(Some(wasm_name.ok_or(
600            ScryptoCompilerError::CargoTargetBinaryResolutionError,
601        )?))
602    }
603
604    // Basing on manifest path returns target directory, target binary WASM path and target binary PRD path
605    fn prepare_paths_for_manifest(
606        input_params: &ScryptoCompilerInputParams,
607        manifest_path: &Path,
608    ) -> Result<CompilerManifestDefinition, ScryptoCompilerError> {
609        // Generate target directory
610        let target_directory = if let Some(directory) = &input_params.target_directory {
611            // If target directory is explicitly specified as compiler parameter then use it as is
612            PathBuf::from(directory)
613        } else {
614            // If target directory is not specified as compiler parameter then get default
615            // target directory basing on manifest file
616            PathBuf::from(&Self::get_default_target_directory(&manifest_path)?)
617        };
618
619        let definition = if let Some(target_binary_name) =
620            Self::get_target_binary_name(&manifest_path)?
621        {
622            // First in phase 1, we build the package with schema extract facilities
623            // This has to be built in the release profile
624            let mut target_phase_1_build_wasm_output_path = target_directory.clone();
625            target_phase_1_build_wasm_output_path.push(BUILD_TARGET);
626            target_phase_1_build_wasm_output_path.push(Profile::Release.as_target_directory_name());
627            target_phase_1_build_wasm_output_path.push(target_binary_name.clone());
628            target_phase_1_build_wasm_output_path.set_extension("wasm");
629
630            let mut target_copied_wasm_with_schema_path = target_directory.clone();
631            target_copied_wasm_with_schema_path.push(BUILD_TARGET);
632            target_copied_wasm_with_schema_path.push(Profile::Release.as_target_directory_name());
633            target_copied_wasm_with_schema_path
634                .push(format!("{}_with_schema", target_binary_name.clone()));
635            target_copied_wasm_with_schema_path.set_extension("wasm");
636
637            // In phase 2, we build the package in the requested profile
638            let mut target_phase_2_build_wasm_output_path = target_directory.clone();
639            target_phase_2_build_wasm_output_path.push(BUILD_TARGET);
640            target_phase_2_build_wasm_output_path
641                .push(input_params.profile.as_target_directory_name());
642            target_phase_2_build_wasm_output_path.push(target_binary_name.clone());
643            target_phase_2_build_wasm_output_path.set_extension("wasm");
644
645            // We output the rpd in the target profile
646            let mut target_output_binary_rpd_path = target_directory.clone();
647            target_output_binary_rpd_path.push(BUILD_TARGET);
648            target_output_binary_rpd_path.push(input_params.profile.as_target_directory_name());
649            target_output_binary_rpd_path.push(target_binary_name.clone());
650            target_output_binary_rpd_path.set_extension("rpd");
651
652            CompilerManifestDefinition {
653                manifest_path: manifest_path.to_path_buf(),
654                target_directory,
655                target_binary_name,
656                target_phase_1_build_wasm_output_path,
657                target_phase_2_build_wasm_output_path,
658                target_output_binary_rpd_path,
659                target_copied_wasm_with_schema_path,
660            }
661        } else {
662            CompilerManifestDefinition {
663                manifest_path: manifest_path.to_path_buf(),
664                target_directory,
665                // for workspace compilation these paths are empty
666                target_binary_name: String::new(),
667                target_phase_1_build_wasm_output_path: PathBuf::new(),
668                target_phase_2_build_wasm_output_path: PathBuf::new(),
669                target_output_binary_rpd_path: PathBuf::new(),
670                target_copied_wasm_with_schema_path: PathBuf::new(),
671            }
672        };
673
674        Ok(definition)
675    }
676
677    // Prepares OS command arguments
678    fn prepare_command(&mut self, command: &mut Command, for_package_extract: bool) {
679        let mut features: Vec<[&str; 2]> = self
680            .input_params
681            .features
682            .iter()
683            .map(|f| ["--features", f])
684            .collect();
685        if let Some(idx) = features
686            .iter()
687            .position(|[_tag, value]| *value == SCRYPTO_NO_SCHEMA)
688        {
689            if for_package_extract {
690                features.remove(idx);
691            }
692        } else if !for_package_extract {
693            features.push(["--features", SCRYPTO_NO_SCHEMA]);
694        }
695
696        let mut remove_cargo_rustflags_env = false;
697        if for_package_extract {
698            if let Some(idx) = features
699                .iter()
700                .position(|[_tag, value]| *value == SCRYPTO_COVERAGE)
701            {
702                // for schema extract 'scrypto/coverage' flag must be removed
703                features.remove(idx);
704                remove_cargo_rustflags_env = true;
705            }
706        }
707
708        let features: Vec<&str> = features.into_iter().flatten().collect();
709
710        let package: Vec<&str> = self
711            .input_params
712            .package
713            .iter()
714            .map(|p| ["--package", p])
715            .flatten()
716            .collect();
717
718        command
719            .arg("build")
720            .arg("--target")
721            .arg(BUILD_TARGET)
722            .arg("--target-dir")
723            .arg(&self.main_manifest.target_directory)
724            .arg("--manifest-path")
725            .arg(&self.main_manifest.manifest_path)
726            .args(package)
727            .args(features);
728
729        if for_package_extract {
730            // At package extract time, we have to use release mode, else we get an error
731            // when running the WASM:
732            // Err(SchemaExtractionError(InvalidWasm(TooManyFunctionLocals { max: 256, actual: 257 })))
733            command.arg("--release");
734        } else {
735            command.args(self.input_params.profile.as_command_args());
736        }
737
738        if self.input_params.no_default_features {
739            command.arg("--no-default-features");
740        }
741        if self.input_params.all_features {
742            command.arg("--all_features");
743        }
744
745        // We support an environment variable to make it easy to turn `--locked` mode
746        // on in CI, without having to rewrite all the code/plumbing.
747        let force_locked =
748            !self.input_params.ignore_locked_env_var && is_scrypto_cargo_locked_env_var_active();
749        if force_locked || self.input_params.locked {
750            command.arg("--locked");
751        }
752
753        self.input_params
754            .environment_variables
755            .iter()
756            .for_each(|(name, action)| {
757                match action {
758                    EnvironmentVariableAction::Set(value) => {
759                        // CARGO_ENCODED_RUSTFLAGS for coverage build must be removed for 1st phase compilation
760                        if !(remove_cargo_rustflags_env && name == "CARGO_ENCODED_RUSTFLAGS") {
761                            command.env(name, value);
762                        }
763                    }
764                    EnvironmentVariableAction::Unset => {
765                        command.env_remove(name);
766                    }
767                };
768            });
769
770        command.args(self.input_params.custom_options.iter());
771    }
772
773    fn wasm_optimize(&self, wasm_path: &Path) -> Result<(), ScryptoCompilerError> {
774        if let Some(wasm_opt_config) = &self.input_params.wasm_optimization {
775            if self.input_params.verbose {
776                println!("Optimizing WASM {:?}", wasm_opt_config);
777            }
778            wasm_opt_config
779                .run(wasm_path, wasm_path)
780                .map_err(ScryptoCompilerError::WasmOptimizationError)
781        } else {
782            Ok(())
783        }
784    }
785
786    // Create scrypto build lock file for each compiled package to protect compilation in case it is invoked multiple times in parallel.
787    fn lock_packages(&self) -> Result<Vec<PackageLock>, ScryptoCompilerError> {
788        let mut package_locks: Vec<PackageLock> = vec![];
789        // Create target folder if it doesn't exist
790        std::fs::create_dir_all(&self.main_manifest.target_directory).map_err(|err| {
791            ScryptoCompilerError::IOErrorWithPath(
792                err,
793                self.main_manifest.target_directory.clone(),
794                Some(String::from("Create target folder failed")),
795            )
796        })?;
797
798        // Collect packages to be locked
799        for package in self
800            .iter_manifests()
801            .map(|manifest| &manifest.target_binary_name)
802        {
803            let lock_file_path = self
804                .main_manifest
805                .target_directory
806                .join(format!("{}.lock", package));
807            let package_lock = PackageLock::new(lock_file_path)?;
808            package_locks.push(package_lock);
809        }
810        package_locks.sort();
811
812        let mut all_locked = false;
813        // Attempt to lock all compiled packages.
814        while !all_locked {
815            all_locked = true;
816            for package_lock in package_locks.iter_mut() {
817                if !package_lock.is_locked() {
818                    if !package_lock.try_lock()? {
819                        all_locked = false;
820                    }
821                }
822            }
823
824            // Unlock if not all packages locked.
825            // We need all packages to be locked at once to make sure
826            // no other thread locked some package in the meantime.
827            if !all_locked {
828                for package_lock in package_locks.iter_mut() {
829                    if package_lock.is_locked() {
830                        package_lock.unlock()?;
831                    }
832                }
833            }
834
835            // Give CPU some rest - sleep for 10ms
836            std::thread::sleep(std::time::Duration::from_millis(10));
837        }
838
839        Ok(package_locks)
840    }
841
842    // Unlock packages
843    fn unlock_packages(&self, package_locks: Vec<PackageLock>) -> Result<(), ScryptoCompilerError> {
844        for mut package_lock in package_locks {
845            package_lock.unlock()?;
846        }
847        Ok(())
848    }
849
850    fn iter_manifests<'a>(&self) -> impl Iterator<Item = &CompilerManifestDefinition> {
851        if self.manifests.is_empty() {
852            Either::Left(iter::once(&self.main_manifest))
853        } else {
854            Either::Right(self.manifests.iter())
855        }
856    }
857
858    // Scrypto compilation flow:
859    //  - Compile with schema (without "scrypto/no-schema" feature) and release profile.
860    //    Rename WASM files from '*.wasm' to '*_with_schema.wasm'
861    //  - Try to get the remaining build artifacts (optimized WASM without schema '*.wasm' and '*.rpd' files) from Scrypto cache.
862    //    It is done by calculating hash of the '*_with_schema.wasm' and searching its
863    //  - If no files in Scrypto cache then:
864    //    - Extract schema from '*_with_schema.wasm' into '*.rpd' files
865    //    - Compile (with "scrypto/no-schema" feature) and optionally optimize WASM files '*.wasm'
866    //    - Store '*.wasm' and '*.rpd' in Scrypto cache
867    pub fn compile_with_stdio<T: Into<Stdio>>(
868        &mut self,
869        stdin: Option<T>,
870        stdout: Option<T>,
871        stderr: Option<T>,
872    ) -> Result<Vec<BuildArtifacts>, ScryptoCompilerError> {
873        let package_locks = self.lock_packages()?;
874
875        let mut command = Command::new("cargo");
876        // Stdio streams used only for 1st phase compilation due to lack of Copy trait.
877        if let Some(s) = stdin {
878            command.stdin(s);
879        }
880        if let Some(s) = stdout {
881            command.stdout(s);
882        }
883        if let Some(s) = stderr {
884            command.stderr(s);
885        }
886
887        self.compile_phase_1(&mut command)?;
888
889        // For simplicity, do not use cache if coverage enabled
890        let artifacts = if self.input_params.features.get(SCRYPTO_COVERAGE).is_none() {
891            self.get_artifacts_from_cache()?
892        } else {
893            vec![]
894        };
895
896        let artifacts = if artifacts.is_empty() {
897            let mut command = Command::new("cargo");
898            self.compile_phase_2(&mut command)?
899        } else {
900            artifacts
901        };
902
903        self.unlock_packages(package_locks)?;
904        Ok(artifacts)
905    }
906
907    pub fn compile(&mut self) -> Result<Vec<BuildArtifacts>, ScryptoCompilerError> {
908        self.compile_with_stdio::<Stdio>(None, None, None)
909    }
910
911    // Compile with schema
912    fn compile_phase_1(&mut self, command: &mut Command) -> Result<(), ScryptoCompilerError> {
913        self.prepare_command_phase_1(command);
914        self.cargo_command_call(command)?;
915
916        for manifest in self.iter_manifests() {
917            self.compile_phase_1_postprocess(&manifest)?;
918        }
919
920        Ok(())
921    }
922
923    // Rename WASM files from '*.wasm' to '*_with_schema.wasm'
924    fn compile_phase_1_postprocess(
925        &self,
926        manifest_def: &CompilerManifestDefinition,
927    ) -> Result<(), ScryptoCompilerError> {
928        // The best would be to directly produce wasm file with schema by overriding Cargo.toml
929        // values from command line.
930        // Possibly it could be done by replacing 'cargo build' with 'cargo rustc' command,
931        // which allows to customize settings on lower level. It is very likely it would implicate
932        // more changes. And we don't want to complicate things more. So lets just rename the file.
933        std::fs::rename(
934            &manifest_def.target_phase_1_build_wasm_output_path,
935            &manifest_def.target_copied_wasm_with_schema_path,
936        )
937        .map_err(|err| {
938            ScryptoCompilerError::IOErrorWithPath(
939                err,
940                manifest_def.target_phase_1_build_wasm_output_path.clone(),
941                Some(String::from("Rename WASM file failed.")),
942            )
943        })?;
944        Ok(())
945    }
946
947    // used for unit tests
948    fn prepare_command_phase_2(&mut self, command: &mut Command) {
949        self.prepare_command(command, false); // build without schema and with userchoosen profile
950    }
951
952    // Compile without schema and with optional wasm optimisations - this is the final .wasm file
953    fn compile_phase_2(
954        &mut self,
955        command: &mut Command,
956    ) -> Result<Vec<BuildArtifacts>, ScryptoCompilerError> {
957        self.prepare_command_phase_2(command);
958        self.cargo_command_call(command)?;
959
960        Ok(self
961            .iter_manifests()
962            .map(|manifest| self.compile_phase_2_postprocess(&manifest))
963            .collect::<Result<Vec<_>, ScryptoCompilerError>>()?)
964    }
965
966    // Extract schema, optionally optimize WASM, store artifacts in cache
967    fn compile_phase_2_postprocess(
968        &self,
969        manifest_def: &CompilerManifestDefinition,
970    ) -> Result<BuildArtifacts, ScryptoCompilerError> {
971        // TODO: code was already read to calculate hash. Optimize it.
972        let code =
973            std::fs::read(&manifest_def.target_copied_wasm_with_schema_path).map_err(|e| {
974                ScryptoCompilerError::IOErrorWithPath(
975                    e,
976                    manifest_def.target_copied_wasm_with_schema_path.clone(),
977                    Some(String::from("Read WASM file for RPD extract failed.")),
978                )
979            })?;
980        let code_hash = hash(&code);
981
982        let package_definition =
983            extract_definition(&code).map_err(ScryptoCompilerError::SchemaExtractionError)?;
984
985        std::fs::write(
986            &manifest_def.target_output_binary_rpd_path,
987            manifest_encode(&package_definition)
988                .map_err(ScryptoCompilerError::SchemaEncodeError)?,
989        )
990        .map_err(|err| {
991            ScryptoCompilerError::IOErrorWithPath(
992                err,
993                manifest_def.target_output_binary_rpd_path.clone(),
994                Some(String::from("RPD file write failed.")),
995            )
996        })?;
997
998        self.wasm_optimize(&manifest_def.target_phase_2_build_wasm_output_path.clone())?;
999
1000        let code =
1001            std::fs::read(&manifest_def.target_phase_2_build_wasm_output_path).map_err(|e| {
1002                ScryptoCompilerError::IOErrorWithPath(
1003                    e,
1004                    manifest_def.target_phase_2_build_wasm_output_path.clone(),
1005                    Some(String::from("Read optimized WASM file failed.")),
1006                )
1007            })?;
1008
1009        let package_definition = BuildArtifact {
1010            path: manifest_def.target_output_binary_rpd_path.clone(),
1011            content: package_definition,
1012        };
1013        let wasm = BuildArtifact {
1014            path: manifest_def.target_phase_2_build_wasm_output_path.clone(),
1015            content: code,
1016        };
1017        let artifacts = BuildArtifacts {
1018            wasm,
1019            package_definition,
1020        };
1021
1022        self.store_artifacts_in_cache(manifest_def, code_hash, &artifacts)?;
1023
1024        Ok(artifacts)
1025    }
1026
1027    fn cargo_command_call(&mut self, command: &mut Command) -> Result<(), ScryptoCompilerError> {
1028        if self.input_params.verbose {
1029            println!("Executing command: {}", cmd_to_string(command));
1030        }
1031        let status = command.status().map_err(|e| {
1032            ScryptoCompilerError::IOError(e, Some(String::from("Cargo build command failed.")))
1033        })?;
1034        status
1035            .success()
1036            .then_some(())
1037            .ok_or(ScryptoCompilerError::CargoBuildFailure(status))
1038    }
1039
1040    // Return paths to the Scrypto cache for given manifest deifinition and code hash
1041    fn get_scrypto_cache_paths(
1042        &self,
1043        manifest_def: &CompilerManifestDefinition,
1044        code_hash: Hash,
1045        create_if_not_exists: bool,
1046    ) -> Result<(PathBuf, PathBuf), ScryptoCompilerError> {
1047        // WASM optimizations are optional and might be configured on different ways.
1048        // They are applied in 2nd compilation, which means one can receive different WASMs
1049        // for the same WASM files from 1st compilation.
1050        let options = format!(
1051            "{:?}/{:?}/{:?}",
1052            code_hash,
1053            self.input_params.profile.as_target_directory_name(),
1054            self.input_params.wasm_optimization
1055        );
1056        let hash_dir = hash(options);
1057
1058        let cache_path = manifest_def
1059            .target_directory
1060            .join("scrypto_cache")
1061            .join(hash_dir.to_string());
1062
1063        if create_if_not_exists {
1064            // Create target folder if it doesn't exist
1065            std::fs::create_dir_all(&cache_path).map_err(|err| {
1066                ScryptoCompilerError::IOErrorWithPath(
1067                    err,
1068                    cache_path.clone(),
1069                    Some(String::from("Create cache folder failed")),
1070                )
1071            })?;
1072        }
1073
1074        let mut rpd_cache_path = cache_path
1075            .clone()
1076            .join(manifest_def.target_binary_name.clone());
1077        rpd_cache_path.set_extension("rpd");
1078
1079        let mut wasm_cache_path = cache_path.join(manifest_def.target_binary_name.clone());
1080        wasm_cache_path.set_extension("wasm");
1081        Ok((rpd_cache_path, wasm_cache_path))
1082    }
1083
1084    // Store build artifacts in Scrypto cache.
1085    // Override existing entries.
1086    fn store_artifacts_in_cache(
1087        &self,
1088        manifest_def: &CompilerManifestDefinition,
1089        code_hash: Hash,
1090        artifacts: &BuildArtifacts,
1091    ) -> Result<(), ScryptoCompilerError> {
1092        let (rpd_cache_path, wasm_cache_path) =
1093            self.get_scrypto_cache_paths(manifest_def, code_hash, true)?;
1094
1095        std::fs::copy(&artifacts.package_definition.path, &rpd_cache_path).map_err(|err| {
1096            ScryptoCompilerError::IOErrorWithPath(
1097                err,
1098                artifacts.package_definition.path.clone(),
1099                Some(String::from("Copy RPD into cache folder failed")),
1100            )
1101        })?;
1102
1103        std::fs::copy(&artifacts.wasm.path, &wasm_cache_path).map_err(|err| {
1104            ScryptoCompilerError::IOErrorWithPath(
1105                err,
1106                artifacts.wasm.path.clone(),
1107                Some(String::from("Copy WASM file into cache folder failed")),
1108            )
1109        })?;
1110
1111        Ok(())
1112    }
1113
1114    // Collect build artifacts from Scrypto cache.
1115    fn get_artifacts_from_cache(&mut self) -> Result<Vec<BuildArtifacts>, ScryptoCompilerError> {
1116        // compilation post-processing for all manifests
1117        let mut artifacts = vec![];
1118        for manifest in self.iter_manifests() {
1119            let artifact = self.get_artifact_from_cache_for_manifest(manifest)?;
1120
1121            // If artifact for any manifest is missing then assume no artifacts in cache at all
1122            if let Some(artifact) = artifact {
1123                artifacts.push(artifact);
1124            } else {
1125                return Ok(vec![]);
1126            }
1127        }
1128
1129        Ok(artifacts)
1130    }
1131
1132    // Collect build artifacts from Scrypto cache for given manifest definition.
1133    fn get_artifact_from_cache_for_manifest(
1134        &self,
1135        manifest_def: &CompilerManifestDefinition,
1136    ) -> Result<Option<BuildArtifacts>, ScryptoCompilerError> {
1137        let code =
1138            std::fs::read(&manifest_def.target_copied_wasm_with_schema_path).map_err(|e| {
1139                ScryptoCompilerError::IOErrorWithPath(
1140                    e,
1141                    manifest_def.target_copied_wasm_with_schema_path.clone(),
1142                    Some(String::from("Read WASM with schema file failed.")),
1143                )
1144            })?;
1145        let code_hash = hash(&code);
1146
1147        let (rpd_cache_path, wasm_cache_path) =
1148            self.get_scrypto_cache_paths(manifest_def, code_hash, false)?;
1149
1150        // Get WASM and RPD files only if they both exist
1151        if std::fs::metadata(&rpd_cache_path).is_ok() && std::fs::metadata(&wasm_cache_path).is_ok()
1152        {
1153            let rpd = std::fs::read(&rpd_cache_path).map_err(|e| {
1154                ScryptoCompilerError::IOErrorWithPath(
1155                    e,
1156                    rpd_cache_path.clone(),
1157                    Some(String::from("Read RPD from cache failed.")),
1158                )
1159            })?;
1160
1161            let package_definition: PackageDefinition =
1162                manifest_decode(&rpd).map_err(ScryptoCompilerError::SchemaDecodeError)?;
1163
1164            let wasm = std::fs::read(&wasm_cache_path).map_err(|e| {
1165                ScryptoCompilerError::IOErrorWithPath(
1166                    e,
1167                    wasm_cache_path.clone(),
1168                    Some(String::from("Read WASM from cache failed.")),
1169                )
1170            })?;
1171
1172            // Store artifacts into release folder
1173            let rpd_output_parent = manifest_def.target_output_binary_rpd_path.parent().unwrap();
1174            std::fs::create_dir_all(rpd_output_parent).map_err(|e| {
1175                ScryptoCompilerError::IOErrorWithPath(
1176                    e,
1177                    rpd_output_parent.to_path_buf(),
1178                    Some(String::from(
1179                        "Error creating the RPD file's parent folder if it doesn't exist.",
1180                    )),
1181                )
1182            })?;
1183            std::fs::write(&manifest_def.target_output_binary_rpd_path, rpd).map_err(|e| {
1184                ScryptoCompilerError::IOErrorWithPath(
1185                    e,
1186                    manifest_def.target_output_binary_rpd_path.clone(),
1187                    Some(String::from("Write RPD file failed.")),
1188                )
1189            })?;
1190
1191            // On filesystems with hard-linking support `target_binary_wasm_path` might be a hard-link
1192            // (rust caching for incremental builds)
1193            // pointing to `./<target-dir>/wasm32-unknown-unknown/release/deps/<wasm_binary>`,
1194            // which would be also modified if we would directly wrote below data.
1195            // Which in turn would be reused in the next recompilation resulting with a
1196            // `target_binary_wasm_with_schema_path` not including the schema.
1197            // So if `target_binary_wasm_path` exists just remove it assuming it is a hard-link.
1198            let wasm_output_parent = manifest_def
1199                .target_phase_2_build_wasm_output_path
1200                .parent()
1201                .unwrap();
1202            std::fs::create_dir_all(wasm_output_parent).map_err(|e| {
1203                ScryptoCompilerError::IOErrorWithPath(
1204                    e,
1205                    wasm_output_parent.to_path_buf(),
1206                    Some(String::from(
1207                        "Error creating the WASM file's parent folder if it doesn't exist.",
1208                    )),
1209                )
1210            })?;
1211            if std::fs::metadata(&manifest_def.target_phase_2_build_wasm_output_path).is_ok() {
1212                std::fs::remove_file(&manifest_def.target_phase_2_build_wasm_output_path).map_err(
1213                    |e| {
1214                        ScryptoCompilerError::IOErrorWithPath(
1215                            e,
1216                            manifest_def.target_phase_2_build_wasm_output_path.clone(),
1217                            Some(String::from("Remove WASM file failed.")),
1218                        )
1219                    },
1220                )?;
1221            }
1222            std::fs::write(
1223                &manifest_def.target_phase_2_build_wasm_output_path,
1224                wasm.clone(),
1225            )
1226            .map_err(|e| {
1227                ScryptoCompilerError::IOErrorWithPath(
1228                    e,
1229                    manifest_def.target_phase_2_build_wasm_output_path.clone(),
1230                    Some(String::from("Write WASM file failed.")),
1231                )
1232            })?;
1233
1234            let wasm = BuildArtifact {
1235                path: manifest_def.target_phase_2_build_wasm_output_path.clone(),
1236                content: wasm,
1237            };
1238            let package_definition = BuildArtifact {
1239                path: manifest_def.target_output_binary_rpd_path.clone(),
1240                content: package_definition,
1241            };
1242
1243            Ok(Some(BuildArtifacts {
1244                wasm,
1245                package_definition,
1246            }))
1247        } else {
1248            Ok(None)
1249        }
1250    }
1251
1252    // used for unit tests
1253    fn prepare_command_phase_1(&mut self, command: &mut Command) {
1254        self.prepare_command(command, true); // build with schema and release profile
1255    }
1256
1257    /// Returns information about the main manifest
1258    pub fn get_main_manifest_definition(&self) -> CompilerManifestDefinition {
1259        self.main_manifest.clone()
1260    }
1261}
1262
1263#[derive(Default)]
1264pub struct ScryptoCompilerBuilder {
1265    input_params: ScryptoCompilerInputParams,
1266}
1267
1268impl ScryptoCompilerBuilder {
1269    pub fn manifest_path(&mut self, path: impl Into<PathBuf>) -> &mut Self {
1270        self.input_params.manifest_path = Some(path.into());
1271        self
1272    }
1273
1274    pub fn target_directory(&mut self, directory: impl Into<PathBuf>) -> &mut Self {
1275        self.input_params.target_directory = Some(directory.into());
1276
1277        self
1278    }
1279
1280    pub fn profile(&mut self, profile: Profile) -> &mut Self {
1281        self.input_params.profile = profile;
1282        self
1283    }
1284
1285    pub fn env(&mut self, name: &str, action: EnvironmentVariableAction) -> &mut Self {
1286        self.input_params
1287            .environment_variables
1288            .insert(name.to_string(), action);
1289        self
1290    }
1291
1292    pub fn feature(&mut self, name: &str) -> &mut Self {
1293        self.input_params.features.insert(name.to_string());
1294        self
1295    }
1296
1297    pub fn no_default_features(&mut self) -> &mut Self {
1298        self.input_params.no_default_features = true;
1299        self
1300    }
1301
1302    pub fn all_features(&mut self) -> &mut Self {
1303        self.input_params.all_features = true;
1304        self
1305    }
1306
1307    pub fn locked(&mut self) -> &mut Self {
1308        self.input_params.locked = true;
1309        self
1310    }
1311
1312    pub fn ignore_locked_env_var(&mut self) -> &mut Self {
1313        self.input_params.ignore_locked_env_var = true;
1314        self
1315    }
1316
1317    pub fn package(&mut self, name: &str) -> &mut Self {
1318        self.input_params.package.insert(name.to_string());
1319        self
1320    }
1321
1322    pub fn scrypto_macro_trace(&mut self) -> &mut Self {
1323        self.input_params
1324            .features
1325            .insert(String::from("scrypto/trace"));
1326        self
1327    }
1328
1329    pub fn log_level(&mut self, log_level: Level) -> &mut Self {
1330        // Firstly clear any log level previously set
1331        let all_features = ScryptoCompilerInputParams::log_level_to_scrypto_features(Level::Trace);
1332        all_features.iter().for_each(|log_level| {
1333            self.input_params.features.swap_remove(log_level);
1334        });
1335
1336        // Now set log level provided by the user
1337        if Level::Error <= log_level {
1338            self.input_params
1339                .features
1340                .insert(String::from("scrypto/log-error"));
1341        }
1342        if Level::Warn <= log_level {
1343            self.input_params
1344                .features
1345                .insert(String::from("scrypto/log-warn"));
1346        }
1347        if Level::Info <= log_level {
1348            self.input_params
1349                .features
1350                .insert(String::from("scrypto/log-info"));
1351        }
1352        if Level::Debug <= log_level {
1353            self.input_params
1354                .features
1355                .insert(String::from("scrypto/log-debug"));
1356        }
1357        if Level::Trace <= log_level {
1358            self.input_params
1359                .features
1360                .insert(String::from("scrypto/log-trace"));
1361        }
1362        self
1363    }
1364
1365    pub fn coverage(&mut self) -> &mut Self {
1366        self.input_params
1367            .features
1368            .insert(String::from(SCRYPTO_COVERAGE));
1369        self
1370    }
1371
1372    pub fn optimize_with_wasm_opt(
1373        &mut self,
1374        options: Option<wasm_opt::OptimizationOptions>,
1375    ) -> &mut Self {
1376        self.input_params.wasm_optimization = options;
1377        self
1378    }
1379
1380    pub fn custom_options(&mut self, options: &[&str]) -> &mut Self {
1381        self.input_params
1382            .custom_options
1383            .extend(options.iter().map(|item| item.to_string()));
1384        self
1385    }
1386
1387    pub fn debug(&mut self, verbose: bool) -> &mut Self {
1388        self.input_params.verbose = verbose;
1389        self
1390    }
1391
1392    pub fn build(&mut self) -> Result<ScryptoCompiler, ScryptoCompilerError> {
1393        ScryptoCompiler::from_input_params(&mut self.input_params)
1394    }
1395
1396    pub fn compile(&mut self) -> Result<Vec<BuildArtifacts>, ScryptoCompilerError> {
1397        self.build()?.compile()
1398    }
1399
1400    pub fn compile_with_stdio<T: Into<Stdio>>(
1401        &mut self,
1402        stdin: Option<T>,
1403        stdout: Option<T>,
1404        stderr: Option<T>,
1405    ) -> Result<Vec<BuildArtifacts>, ScryptoCompilerError> {
1406        self.build()?.compile_with_stdio(stdin, stdout, stderr)
1407    }
1408}
1409
1410#[cfg(feature = "std")]
1411pub fn is_scrypto_cargo_locked_env_var_active() -> bool {
1412    std::env::var("SCRYPTO_CARGO_LOCKED").is_ok_and(|val| {
1413        let normalized = val.to_lowercase();
1414        &normalized == "true" || &normalized == "1"
1415    })
1416}
1417
1418#[cfg(not(feature = "std"))]
1419pub fn is_scrypto_cargo_locked_env_var_active() -> bool {
1420    false
1421}
1422
1423// helper function
1424fn cmd_to_string(cmd: &Command) -> String {
1425    let args = cmd
1426        .get_args()
1427        .into_iter()
1428        .map(|arg| arg.to_str().unwrap())
1429        .collect::<Vec<_>>()
1430        .join(" ");
1431    let envs = cmd
1432        .get_envs()
1433        .into_iter()
1434        .map(|(name, value)| {
1435            if let Some(value) = value {
1436                format!("{}='{}'", name.to_str().unwrap(), value.to_str().unwrap())
1437            } else {
1438                format!("{}", name.to_str().unwrap())
1439            }
1440        })
1441        .collect::<Vec<_>>()
1442        .join(" ");
1443    let mut ret = envs;
1444    if !ret.is_empty() {
1445        ret.push(' ');
1446    }
1447    ret.push_str(cmd.get_program().to_str().unwrap());
1448    ret.push(' ');
1449    ret.push_str(&args);
1450    ret
1451}
1452
1453#[cfg(test)]
1454mod tests {
1455    use super::*;
1456
1457    #[test]
1458    fn test_target_binary_path_target() {
1459        let target_dir = "./tests/target";
1460        let compiler = ScryptoCompiler::builder()
1461            .manifest_path("./tests/assets/scenario_1/blueprint")
1462            .target_directory(target_dir)
1463            .custom_options(&["-j", "1"])
1464            .build()
1465            .unwrap();
1466
1467        assert_eq!(
1468            "./tests/target/wasm32-unknown-unknown/release/test_blueprint.wasm",
1469            compiler
1470                .main_manifest
1471                .target_phase_1_build_wasm_output_path
1472                .display()
1473                .to_string()
1474        );
1475    }
1476
1477    #[test]
1478    fn test_command_output_default() {
1479        // Arrange
1480        let mut manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1481        let mut default_target_path = manifest_path.clone();
1482        manifest_path.push("Cargo.toml");
1483        default_target_path.pop(); // ScryptoCompiler dir
1484        default_target_path.push("target");
1485        let mut cmd_phase_1 = Command::new("cargo");
1486        let mut cmd_phase_2 = Command::new("cargo");
1487
1488        // Act
1489        ScryptoCompiler::builder()
1490            .ignore_locked_env_var()
1491            .build()
1492            .unwrap()
1493            .prepare_command_phase_1(&mut cmd_phase_1);
1494        ScryptoCompiler::builder()
1495            .ignore_locked_env_var()
1496            .build()
1497            .unwrap()
1498            .prepare_command_phase_2(&mut cmd_phase_2);
1499
1500        // Assert
1501        assert_eq!(cmd_to_string(&cmd_phase_1),
1502            format!("TARGET_CFLAGS='-mcpu=mvp -mmutable-globals -msign-ext' cargo build --target wasm32-unknown-unknown --target-dir {} --manifest-path {} --features scrypto/log-error --features scrypto/log-warn --features scrypto/log-info --release", default_target_path.display(), manifest_path.display()));
1503        assert_eq!(cmd_to_string(&cmd_phase_2),
1504            format!("TARGET_CFLAGS='-mcpu=mvp -mmutable-globals -msign-ext' cargo build --target wasm32-unknown-unknown --target-dir {} --manifest-path {} --features scrypto/log-error --features scrypto/log-warn --features scrypto/log-info --features scrypto/no-schema --profile release", default_target_path.display(), manifest_path.display()));
1505    }
1506
1507    #[test]
1508    fn test_command_output_with_manifest_path() {
1509        // Arrange
1510        let mut manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1511        let mut default_target_path = manifest_path.clone();
1512        manifest_path.push("tests/assets/scenario_1/blueprint/Cargo.toml");
1513        default_target_path.push("tests/assets/scenario_1/target");
1514        let mut cmd_phase_1 = Command::new("cargo");
1515        let mut cmd_phase_2 = Command::new("cargo");
1516
1517        // Act
1518        ScryptoCompiler::builder()
1519            .manifest_path(&manifest_path)
1520            .ignore_locked_env_var()
1521            .build()
1522            .unwrap()
1523            .prepare_command_phase_1(&mut cmd_phase_1);
1524        ScryptoCompiler::builder()
1525            .manifest_path(&manifest_path)
1526            .ignore_locked_env_var()
1527            .build()
1528            .unwrap()
1529            .prepare_command_phase_2(&mut cmd_phase_2);
1530
1531        // Assert
1532        assert_eq!(cmd_to_string(&cmd_phase_1),
1533            format!("TARGET_CFLAGS='-mcpu=mvp -mmutable-globals -msign-ext' cargo build --target wasm32-unknown-unknown --target-dir {} --manifest-path {} --features scrypto/log-error --features scrypto/log-warn --features scrypto/log-info --release", default_target_path.display(), manifest_path.display()));
1534        assert_eq!(cmd_to_string(&cmd_phase_2),
1535            format!("TARGET_CFLAGS='-mcpu=mvp -mmutable-globals -msign-ext' cargo build --target wasm32-unknown-unknown --target-dir {} --manifest-path {} --features scrypto/log-error --features scrypto/log-warn --features scrypto/log-info --features scrypto/no-schema --profile release", default_target_path.display(), manifest_path.display()));
1536    }
1537
1538    #[test]
1539    fn test_command_output_target_directory() {
1540        // Arrange
1541        let mut manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1542        manifest_path.push("Cargo.toml");
1543        let target_path = PathBuf::from("/tmp/build");
1544        let mut cmd_phase_1 = Command::new("cargo");
1545        let mut cmd_phase_2 = Command::new("cargo");
1546
1547        // Act
1548        ScryptoCompiler::builder()
1549            .target_directory(&target_path)
1550            .ignore_locked_env_var()
1551            .build()
1552            .unwrap()
1553            .prepare_command_phase_1(&mut cmd_phase_1);
1554        ScryptoCompiler::builder()
1555            .target_directory(&target_path)
1556            .ignore_locked_env_var()
1557            .build()
1558            .unwrap()
1559            .prepare_command_phase_2(&mut cmd_phase_2);
1560
1561        // Assert
1562        assert_eq!(cmd_to_string(&cmd_phase_1),
1563            format!("TARGET_CFLAGS='-mcpu=mvp -mmutable-globals -msign-ext' cargo build --target wasm32-unknown-unknown --target-dir {} --manifest-path {} --features scrypto/log-error --features scrypto/log-warn --features scrypto/log-info --release", target_path.display(), manifest_path.display()));
1564        assert_eq!(cmd_to_string(&cmd_phase_2),
1565            format!("TARGET_CFLAGS='-mcpu=mvp -mmutable-globals -msign-ext' cargo build --target wasm32-unknown-unknown --target-dir {} --manifest-path {} --features scrypto/log-error --features scrypto/log-warn --features scrypto/log-info --features scrypto/no-schema --profile release", target_path.display(), manifest_path.display()));
1566    }
1567
1568    #[test]
1569    fn test_command_output_features() {
1570        // Arrange
1571        let mut manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1572        let mut default_target_path = manifest_path.clone();
1573        manifest_path.push("Cargo.toml");
1574        default_target_path.pop(); // ScryptoCompiler dir
1575        default_target_path.push("target");
1576        let mut cmd_phase_1 = Command::new("cargo");
1577        let mut cmd_phase_2 = Command::new("cargo");
1578
1579        // Act
1580        ScryptoCompiler::builder()
1581            .log_level(Level::Trace)
1582            .feature("feature_1")
1583            .no_default_features()
1584            .ignore_locked_env_var()
1585            .build()
1586            .unwrap()
1587            .prepare_command_phase_1(&mut cmd_phase_1);
1588        ScryptoCompiler::builder()
1589            .log_level(Level::Trace)
1590            .feature("feature_1")
1591            .no_default_features()
1592            .ignore_locked_env_var()
1593            .build()
1594            .unwrap()
1595            .prepare_command_phase_2(&mut cmd_phase_2);
1596
1597        // Assert
1598        assert_eq!(cmd_to_string(&cmd_phase_1),
1599            format!("TARGET_CFLAGS='-mcpu=mvp -mmutable-globals -msign-ext' cargo build --target wasm32-unknown-unknown --target-dir {} --manifest-path {} --features scrypto/log-error --features scrypto/log-warn --features scrypto/log-info --features scrypto/log-debug --features scrypto/log-trace --features feature_1 --release --no-default-features", default_target_path.display(), manifest_path.display()));
1600        assert_eq!(cmd_to_string(&cmd_phase_2),
1601            format!("TARGET_CFLAGS='-mcpu=mvp -mmutable-globals -msign-ext' cargo build --target wasm32-unknown-unknown --target-dir {} --manifest-path {} --features scrypto/log-error --features scrypto/log-warn --features scrypto/log-info --features scrypto/log-debug --features scrypto/log-trace --features feature_1 --features scrypto/no-schema --profile release --no-default-features", default_target_path.display(), manifest_path.display()));
1602    }
1603
1604    #[test]
1605    fn test_command_output_lower_log_level_than_default() {
1606        // Arrange
1607        let mut manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1608        let mut default_target_path = manifest_path.clone();
1609        manifest_path.push("Cargo.toml");
1610        default_target_path.pop(); // ScryptoCompiler dir
1611        default_target_path.push("target");
1612        let mut cmd_phase_1 = Command::new("cargo");
1613        let mut cmd_phase_2 = Command::new("cargo");
1614
1615        // Act
1616        ScryptoCompiler::builder()
1617            .log_level(Level::Error)
1618            .ignore_locked_env_var()
1619            .build()
1620            .unwrap()
1621            .prepare_command_phase_1(&mut cmd_phase_1);
1622        ScryptoCompiler::builder()
1623            .log_level(Level::Error)
1624            .ignore_locked_env_var()
1625            .build()
1626            .unwrap()
1627            .prepare_command_phase_2(&mut cmd_phase_2);
1628
1629        // Assert
1630        assert_eq!(cmd_to_string(&cmd_phase_1),
1631            format!("TARGET_CFLAGS='-mcpu=mvp -mmutable-globals -msign-ext' cargo build --target wasm32-unknown-unknown --target-dir {} --manifest-path {} --features scrypto/log-error --release", default_target_path.display(), manifest_path.display()));
1632        assert_eq!(cmd_to_string(&cmd_phase_2),
1633            format!("TARGET_CFLAGS='-mcpu=mvp -mmutable-globals -msign-ext' cargo build --target wasm32-unknown-unknown --target-dir {} --manifest-path {} --features scrypto/log-error --features scrypto/no-schema --profile release", default_target_path.display(), manifest_path.display()));
1634    }
1635    #[test]
1636    fn test_command_output_workspace() {
1637        // Arrange
1638        let mut manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1639        let mut default_target_path = manifest_path.clone();
1640        manifest_path.push("tests/assets/scenario_1/Cargo.toml");
1641        default_target_path.push("tests/assets/scenario_1/target");
1642        let mut cmd_phase_1 = Command::new("cargo");
1643        let mut cmd_phase_2 = Command::new("cargo");
1644
1645        // Act
1646        ScryptoCompiler::builder()
1647            .manifest_path(&manifest_path)
1648            .ignore_locked_env_var()
1649            .build()
1650            .unwrap()
1651            .prepare_command_phase_1(&mut cmd_phase_1);
1652        ScryptoCompiler::builder()
1653            .manifest_path(&manifest_path)
1654            .ignore_locked_env_var()
1655            .build()
1656            .unwrap()
1657            .prepare_command_phase_2(&mut cmd_phase_2);
1658
1659        // Assert
1660        assert_eq!(cmd_to_string(&cmd_phase_1),
1661            format!("TARGET_CFLAGS='-mcpu=mvp -mmutable-globals -msign-ext' cargo build --target wasm32-unknown-unknown --target-dir {} --manifest-path {} --package test_blueprint --package test_blueprint_2 --package test_blueprint_3 --features scrypto/log-error --features scrypto/log-warn --features scrypto/log-info --release", default_target_path.display(), manifest_path.display()));
1662        assert_eq!(cmd_to_string(&cmd_phase_2),
1663            format!("TARGET_CFLAGS='-mcpu=mvp -mmutable-globals -msign-ext' cargo build --target wasm32-unknown-unknown --target-dir {} --manifest-path {} --package test_blueprint --package test_blueprint_2 --package test_blueprint_3 --features scrypto/log-error --features scrypto/log-warn --features scrypto/log-info --features scrypto/no-schema --profile release", default_target_path.display(), manifest_path.display()));
1664    }
1665
1666    #[test]
1667    fn test_command_output_workspace_with_packages() {
1668        // Arrange
1669        let mut manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1670        let mut default_target_path = manifest_path.clone();
1671        manifest_path.push("tests/assets/scenario_1/Cargo.toml");
1672        default_target_path.push("tests/assets/scenario_1/target");
1673        let mut cmd_phase_1 = Command::new("cargo");
1674        let mut cmd_phase_2 = Command::new("cargo");
1675
1676        // Act
1677        ScryptoCompiler::builder()
1678            .manifest_path(&manifest_path)
1679            .package("test_blueprint")
1680            .package("test_blueprint_3")
1681            .ignore_locked_env_var()
1682            .build()
1683            .unwrap()
1684            .prepare_command_phase_1(&mut cmd_phase_1);
1685        ScryptoCompiler::builder()
1686            .manifest_path(&manifest_path)
1687            .package("test_blueprint")
1688            .package("test_blueprint_3")
1689            .ignore_locked_env_var()
1690            .build()
1691            .unwrap()
1692            .prepare_command_phase_2(&mut cmd_phase_2);
1693
1694        // Assert
1695        assert_eq!(cmd_to_string(&cmd_phase_1),
1696            format!("TARGET_CFLAGS='-mcpu=mvp -mmutable-globals -msign-ext' cargo build --target wasm32-unknown-unknown --target-dir {} --manifest-path {} --package test_blueprint --package test_blueprint_3 --features scrypto/log-error --features scrypto/log-warn --features scrypto/log-info --release", default_target_path.display(), manifest_path.display()));
1697        assert_eq!(cmd_to_string(&cmd_phase_2),
1698            format!("TARGET_CFLAGS='-mcpu=mvp -mmutable-globals -msign-ext' cargo build --target wasm32-unknown-unknown --target-dir {} --manifest-path {} --package test_blueprint --package test_blueprint_3 --features scrypto/log-error --features scrypto/log-warn --features scrypto/log-info --features scrypto/no-schema --profile release", default_target_path.display(), manifest_path.display()));
1699    }
1700
1701    #[test]
1702    fn test_command_output_profiles() {
1703        // Arrange
1704        let mut manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1705        let mut default_target_path = manifest_path.clone();
1706        manifest_path.push("Cargo.toml");
1707        default_target_path.pop(); // ScryptoCompiler dir
1708        default_target_path.push("target");
1709        let mut cmd_phase_1 = Command::new("cargo");
1710        let mut cmd_phase_2 = Command::new("cargo");
1711
1712        // Act
1713        ScryptoCompiler::builder()
1714            .profile(Profile::Debug)
1715            .ignore_locked_env_var()
1716            .build()
1717            .unwrap()
1718            .prepare_command_phase_1(&mut cmd_phase_1);
1719        ScryptoCompiler::builder()
1720            .profile(Profile::Debug)
1721            .ignore_locked_env_var()
1722            .build()
1723            .unwrap()
1724            .prepare_command_phase_2(&mut cmd_phase_2);
1725
1726        // Assert
1727        assert_eq!(cmd_to_string(&cmd_phase_1),
1728            format!("TARGET_CFLAGS='-mcpu=mvp -mmutable-globals -msign-ext' cargo build --target wasm32-unknown-unknown --target-dir {} --manifest-path {} --features scrypto/log-error --features scrypto/log-warn --features scrypto/log-info --release", default_target_path.display(), manifest_path.display()));
1729        assert_eq!(cmd_to_string(&cmd_phase_2),
1730            format!("TARGET_CFLAGS='-mcpu=mvp -mmutable-globals -msign-ext' cargo build --target wasm32-unknown-unknown --target-dir {} --manifest-path {} --features scrypto/log-error --features scrypto/log-warn --features scrypto/log-info --features scrypto/no-schema --profile dev", default_target_path.display(), manifest_path.display()));
1731    }
1732
1733    #[test]
1734    fn test_command_output_no_schema_check() {
1735        // Arrange
1736        let mut manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1737        let mut default_target_path = manifest_path.clone();
1738        manifest_path.push("Cargo.toml");
1739        default_target_path.pop(); // ScryptoCompiler dir
1740        default_target_path.push("target");
1741        let mut cmd_phase_1 = Command::new("cargo");
1742        let mut cmd_phase_2 = Command::new("cargo");
1743
1744        // Act
1745        // Ensure that no-schema is properly used across both phase compilation, even if specified explicitly by the user.
1746        ScryptoCompiler::builder()
1747            .feature(SCRYPTO_NO_SCHEMA)
1748            .ignore_locked_env_var()
1749            .build()
1750            .unwrap()
1751            .prepare_command_phase_1(&mut cmd_phase_1);
1752        ScryptoCompiler::builder()
1753            .feature(SCRYPTO_NO_SCHEMA)
1754            .ignore_locked_env_var()
1755            .build()
1756            .unwrap()
1757            .prepare_command_phase_2(&mut cmd_phase_2);
1758
1759        // Assert
1760        assert_eq!(cmd_to_string(&cmd_phase_1),
1761            format!("TARGET_CFLAGS='-mcpu=mvp -mmutable-globals -msign-ext' cargo build --target wasm32-unknown-unknown --target-dir {} --manifest-path {} --features scrypto/log-error --features scrypto/log-warn --features scrypto/log-info --release", default_target_path.display(), manifest_path.display()));
1762        assert_eq!(cmd_to_string(&cmd_phase_2),
1763            format!("TARGET_CFLAGS='-mcpu=mvp -mmutable-globals -msign-ext' cargo build --target wasm32-unknown-unknown --target-dir {} --manifest-path {} --features scrypto/log-error --features scrypto/log-warn --features scrypto/log-info --features scrypto/no-schema --profile release", default_target_path.display(), manifest_path.display()));
1764    }
1765
1766    #[test]
1767    fn test_command_coverage() {
1768        // Arrange
1769        let mut manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1770        let mut target_path = manifest_path.clone();
1771        manifest_path.push("Cargo.toml");
1772        target_path.pop(); // ScryptoCompiler dir
1773        target_path.push("coverage");
1774        let mut cmd_phase_1 = Command::new("cargo");
1775        let mut cmd_phase_2 = Command::new("cargo");
1776
1777        // Act
1778        ScryptoCompiler::builder()
1779            .coverage()
1780            .target_directory(target_path.clone())
1781            .ignore_locked_env_var()
1782            .build()
1783            .unwrap()
1784            .prepare_command_phase_1(&mut cmd_phase_1);
1785        ScryptoCompiler::builder()
1786            .coverage()
1787            .target_directory(target_path.clone())
1788            .ignore_locked_env_var()
1789            .build()
1790            .unwrap()
1791            .prepare_command_phase_2(&mut cmd_phase_2);
1792
1793        // Assert
1794        assert_eq!(cmd_to_string(&cmd_phase_1),
1795            format!("TARGET_CFLAGS='-mcpu=mvp -mmutable-globals -msign-ext' cargo build --target wasm32-unknown-unknown --target-dir {} --manifest-path {} --features scrypto/log-error --features scrypto/log-warn --features scrypto/log-info --release", target_path.display(), manifest_path.display()));
1796        assert_eq!(cmd_to_string(&cmd_phase_2),
1797            format!("TARGET_CFLAGS='-mcpu=mvp -mmutable-globals -msign-ext' cargo build --target wasm32-unknown-unknown --target-dir {} --manifest-path {} --features scrypto/log-error --features scrypto/log-warn --features scrypto/log-info --features scrypto/coverage --features scrypto/no-schema --profile release", target_path.display(), manifest_path.display()));
1798    }
1799
1800    #[test]
1801    fn test_command_coverage_with_env() {
1802        // Arrange
1803        let mut manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1804        let mut target_path = manifest_path.clone();
1805        manifest_path.push("Cargo.toml");
1806        target_path.pop(); // ScryptoCompiler dir
1807        target_path.push("coverage");
1808        let action = EnvironmentVariableAction::Set(String::from(
1809            "-Clto=off\x1f-Cinstrument-coverage\x1f-Zno-profiler-runtime\x1f--emit=llvm-ir",
1810        ));
1811        let mut cmd_phase_1 = Command::new("cargo");
1812        let mut cmd_phase_2 = Command::new("cargo");
1813
1814        // Act
1815        ScryptoCompiler::builder()
1816            .coverage()
1817            .target_directory(target_path.clone())
1818            .ignore_locked_env_var()
1819            .env("CARGO_ENCODED_RUSTFLAGS", action.clone()) // CARGO_ENCODED_RUSTFLAGS must be removed for 1st phase
1820            .build()
1821            .unwrap()
1822            .prepare_command_phase_1(&mut cmd_phase_1);
1823        ScryptoCompiler::builder()
1824            .coverage()
1825            .target_directory(target_path.clone())
1826            .ignore_locked_env_var()
1827            .env("CARGO_ENCODED_RUSTFLAGS", action.clone())
1828            .build()
1829            .unwrap()
1830            .prepare_command_phase_2(&mut cmd_phase_2);
1831
1832        // Assert
1833        assert_eq!(cmd_to_string(&cmd_phase_1),
1834            format!("TARGET_CFLAGS='-mcpu=mvp -mmutable-globals -msign-ext' cargo build --target wasm32-unknown-unknown --target-dir {} --manifest-path {} --features scrypto/log-error --features scrypto/log-warn --features scrypto/log-info --release", target_path.display(), manifest_path.display()));
1835        assert_eq!(cmd_to_string(&cmd_phase_2),
1836            format!("CARGO_ENCODED_RUSTFLAGS='-Clto=off\x1f-Cinstrument-coverage\x1f-Zno-profiler-runtime\x1f--emit=llvm-ir' TARGET_CFLAGS='-mcpu=mvp -mmutable-globals -msign-ext' cargo build --target wasm32-unknown-unknown --target-dir {} --manifest-path {} --features scrypto/log-error --features scrypto/log-warn --features scrypto/log-info --features scrypto/coverage --features scrypto/no-schema --profile release", target_path.display(), manifest_path.display()));
1837    }
1838
1839    #[test]
1840    fn test_parallel_compilation() {
1841        use rayon::iter::{IntoParallelIterator, ParallelIterator};
1842
1843        fn artifacts_hash(artifacts: Vec<BuildArtifacts>) -> Hash {
1844            let mut artifacts = artifacts.clone();
1845
1846            artifacts.sort_by(|a, b| a.wasm.path.cmp(&b.wasm.path));
1847
1848            let wasms: Vec<u8> = artifacts
1849                .iter()
1850                .map(|item| item.wasm.content.clone())
1851                .flatten()
1852                .collect();
1853            hash(wasms)
1854        }
1855
1856        // Arrange
1857        let mut manifest_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1858        manifest_path.push("tests/assets/scenario_1/Cargo.toml");
1859
1860        let mut compiler = ScryptoCompiler::builder()
1861            .manifest_path(&manifest_path)
1862            .package("test_blueprint")
1863            .package("test_blueprint_2")
1864            .build()
1865            .unwrap();
1866
1867        let artifacts = compiler.compile().unwrap();
1868        let reference_wasms_hash = artifacts_hash(artifacts);
1869
1870        // Act
1871        // Run couple of compilations in parallel and compare hash of the build artifacts
1872        // with the reference hash.
1873        let found = (0u64..20u64).into_par_iter().find_map_any(|_| {
1874            let mut compiler = ScryptoCompiler::builder()
1875                .manifest_path(&manifest_path)
1876                .package("test_blueprint")
1877                .package("test_blueprint_2")
1878                .build()
1879                .unwrap();
1880
1881            let artifacts = compiler.compile().unwrap();
1882            if reference_wasms_hash != artifacts_hash(artifacts) {
1883                Some(())
1884            } else {
1885                None
1886            }
1887        });
1888
1889        assert!(found.is_none());
1890    }
1891}