cw_orch_core/contract/
paths.rs

1pub use artifacts_dir::from_workspace;
2pub use artifacts_dir::ArtifactsDir;
3pub use wasm_path::WasmPath;
4
5mod wasm_path {
6    use crate::error::CwEnvError;
7    use cosmwasm_std::{ensure_eq, Checksum};
8    use std::{
9        io::Read,
10        path::{Path, PathBuf},
11    };
12
13    /// Direct path to a `.wasm` file
14    /// Stored as `PathBuf` to avoid lifetimes.
15    /// Can be constructed from [`ArtifactsDir`](super::ArtifactsDir).
16    ///
17    /// # Example
18    /// ```no_run
19    /// use cw_orch_core::contract::WasmPath;
20    ///
21    /// // Create a new WasmPath from a path to a WASM file.
22    /// let wasm_path: WasmPath = WasmPath::new("path/to/contract.wasm").unwrap();
23    ///
24    /// // Calculate the checksum of the WASM file.
25    /// let checksum: cosmwasm_std::Checksum = wasm_path.checksum().unwrap();
26    /// ```
27    #[derive(Debug, Clone)]
28    pub struct WasmPath(PathBuf);
29
30    impl WasmPath {
31        /// Create a new WasmPath from a path to a WASM file.
32        pub fn new(path: impl Into<PathBuf>) -> Result<Self, CwEnvError> {
33            let path: PathBuf = path.into();
34            assert!(
35                path.exists(),
36                "provided path {} does not exist",
37                path.display()
38            );
39            ensure_eq!(
40                path.extension(),
41                Some("wasm".as_ref()),
42                CwEnvError::NotWasm {}
43            );
44            Ok(Self(path))
45        }
46
47        /// Get the path to the WASM file
48        pub fn path(&self) -> &Path {
49            self.0.as_path()
50        }
51
52        /// Calculate the checksum of the WASM file.
53        pub fn checksum(&self) -> Result<Checksum, CwEnvError> {
54            let mut file = std::fs::File::open(self.path())?;
55            let mut wasm = Vec::<u8>::new();
56            file.read_to_end(&mut wasm)?;
57            Ok(Checksum::generate(&wasm))
58        }
59    }
60}
61
62mod artifacts_dir {
63    const ARM_POSTFIX: &str = "-aarch64";
64
65    use super::WasmPath;
66    use crate::{
67        build::BuildPostfix, env::ARTIFACTS_DIR_ENV_NAME, error::CwEnvError, log::local_target,
68        CoreEnvVars,
69    };
70
71    use std::{env, fs, path::PathBuf};
72
73    pub fn find_workspace_dir(start_path: Option<String>) -> ::std::path::PathBuf {
74        let crate_path = start_path.unwrap_or(env!("CARGO_MANIFEST_DIR").to_string());
75        let mut current_dir = ::std::path::PathBuf::from(crate_path);
76        match find_workspace_dir_worker(&mut current_dir) {
77            Some(path) => path,
78            None => current_dir,
79        }
80    }
81
82    fn find_workspace_dir_worker(dir: &mut ::std::path::PathBuf) -> Option<::std::path::PathBuf> {
83        loop {
84            let artifacts_dir = dir.join("artifacts");
85            if ::std::fs::metadata(&artifacts_dir).is_ok() {
86                return Some(dir.clone());
87            }
88            // First we pop the dir
89            if !dir.pop() {
90                return None;
91            }
92        }
93    }
94
95    #[macro_export]
96    /// Creates an [`ArtifactsDir`] from the current workspace by searching the file tree for a directory named `artifacts`.
97    ///
98    /// It does this by reading the CARGO_MANIFEST_DIR environment variable and going up the file tree until it finds the `artifacts` directory.
99    macro_rules! from_workspace {
100        () => {
101            ArtifactsDir::auto(Some(env!("CARGO_MANIFEST_DIR").to_string()))
102        };
103    }
104    pub use from_workspace;
105
106    /// Points to a directory containing WASM files
107    ///
108    /// # Example
109    /// ```no_run
110    /// use cw_orch_core::contract::{ArtifactsDir, WasmPath};
111    /// // Get the artifacts directory from the environment variable `ARTIFACTS_DIR`.
112    /// let artifact_dir = ArtifactsDir::env();
113    ///
114    /// // Or create a new one.
115    /// let artifact_dir = ArtifactsDir::new("path/to/artifacts");
116    ///
117    /// // Get a path to a WASM file that contains the string "my_contract".
118    /// let wasm_path: WasmPath = artifact_dir.find_wasm_path("my_contract").unwrap();
119    /// ```
120    pub struct ArtifactsDir(PathBuf);
121
122    impl ArtifactsDir {
123        /// Get the artifacts directory from the environment variable `ARTIFACTS_DIR`.
124        pub fn env() -> Self {
125            let dir = CoreEnvVars::artifacts_dir()
126                .unwrap_or_else(|| panic!("{} env variable not set", ARTIFACTS_DIR_ENV_NAME));
127            Self::new(dir)
128        }
129
130        /// Creates an artifacts dir by searching for an artifacts directory by going up the file tree from start_path or the current directory
131        pub fn auto(start_path: Option<String>) -> Self {
132            // We find the artifacts dir automatically from the place that this function was invoked
133            let workspace_dir = find_workspace_dir(start_path).join("artifacts");
134            log::debug!(target: &local_target(), "Found artifacts dir at {:?}", workspace_dir);
135            Self::new(workspace_dir)
136        }
137
138        /// Create a new ArtifactsDir from a path to a directory containing WASM files.
139        pub fn new(path: impl Into<PathBuf>) -> Self {
140            let path: PathBuf = path.into();
141            assert!(
142                path.exists(),
143                "provided path {} does not exist",
144                path.display()
145            );
146            Self(path)
147        }
148
149        /// Get the path to the artifacts directory
150        pub fn path(&self) -> &PathBuf {
151            &self.0
152        }
153
154        /// Find a WASM file in the artifacts directory that contains the given name.
155        pub fn find_wasm_path(&self, name: &str) -> Result<WasmPath, CwEnvError> {
156            self.find_wasm_path_with_build_postfix(name, <BuildPostfix>::None)
157        }
158
159        /// Find a WASM file in the artifacts directory that contains the given contract name AND build post-fix.
160        /// If a build with the post-fix is not found, the default build will be used.
161        /// If none of the two are found, an error is returned.
162        pub fn find_wasm_path_with_build_postfix(
163            &self,
164            name: &str,
165            build_postfix: BuildPostfix,
166        ) -> Result<WasmPath, CwEnvError> {
167            let build_postfix: String = build_postfix.into();
168            // Found artifacts priority respected
169
170            let mut wasm_with_postfix = None;
171            let mut arm_wasm_with_postfix = None;
172            let mut default_wasm = None;
173            let mut arm_default_wasm = None;
174
175            for entry in fs::read_dir(self.path())?.flatten() {
176                let path = entry.path();
177                // Skip if not a wasm file
178                if !path.is_file() || path.extension().unwrap_or_default() != "wasm" {
179                    continue;
180                }
181
182                let file_name = path.file_name().unwrap_or_default().to_string_lossy();
183                // Wasm with build postfix, non-ARM
184                if is_artifact_with_build_postfix(&file_name, name, &build_postfix) {
185                    wasm_with_postfix = Some(file_name.into_owned());
186                    // As it's highest priority we just the loop end here
187                    break;
188                }
189
190                // Check other valid filenames
191                if is_arm_artifact_with_build_postfix(&file_name, name, &build_postfix) {
192                    // Wasm with build postfix, ARM
193                    arm_wasm_with_postfix = Some(file_name.into_owned())
194                } else if is_default_artifact(&file_name, name) {
195                    // Wasm without build postfix, non-ARM
196                    default_wasm = Some(file_name.into_owned())
197                } else if is_default_arm_artifact(&file_name, name) {
198                    // Wasm without build postfix, ARM
199                    arm_default_wasm = Some(file_name.into_owned())
200                }
201            }
202
203            let path_str = wasm_with_postfix
204                .or(arm_wasm_with_postfix)
205                .or(default_wasm)
206                .or(arm_default_wasm)
207                .ok_or_else(|| {
208                    CwEnvError::WasmNotFound(
209                        name.to_owned(),
210                        self.path().to_str().unwrap_or_default().to_owned(),
211                    )
212                })?;
213            WasmPath::new(self.path().join(path_str))
214        }
215    }
216
217    fn is_artifact(file_name: &str, contract_name: &str) -> bool {
218        file_name.contains(contract_name)
219    }
220
221    fn is_default_artifact(file_name: &str, contract_name: &str) -> bool {
222        file_name.ends_with(format!("{contract_name}.wasm").as_str())
223    }
224
225    fn is_default_arm_artifact(file_name: &str, contract_name: &str) -> bool {
226        file_name.ends_with(format!("{contract_name}{ARM_POSTFIX}.wasm").as_str())
227    }
228
229    fn is_artifact_with_build_postfix(
230        file_name: &str,
231        contract_name: &str,
232        build_postfix: &str,
233    ) -> bool {
234        is_artifact(file_name, contract_name)
235            && file_name.ends_with(format!("{build_postfix}.wasm").as_str())
236    }
237
238    fn is_arm_artifact_with_build_postfix(
239        file_name: &str,
240        contract_name: &str,
241        build_postfix: &str,
242    ) -> bool {
243        is_artifact(file_name, contract_name)
244            && file_name.ends_with(format!("{build_postfix}{ARM_POSTFIX}.wasm").as_str())
245    }
246}