1use crate::Error;
4use duct::cmd;
5use pop_common::{Docker, Profile, manifest::from_path};
6use std::{
7 env, fs,
8 path::{Path, PathBuf},
9};
10
11const DEFAULT_IMAGE: &str = "docker.io/paritytech/srtool";
12const SRTOOL_TAG_URL: &str =
13 "https://raw.githubusercontent.com/paritytech/srtool/master/RUSTC_VERSION";
14
15pub struct DeterministicBuilder {
18 cache_mount: String,
20 default_features: String,
22 digest: String,
24 image: String,
26 package: String,
28 path: PathBuf,
30 profile: Profile,
32 runtime_dir: PathBuf,
34 tag: String,
36}
37
38impl DeterministicBuilder {
39 pub async fn new(
48 path: Option<PathBuf>,
49 package: &str,
50 profile: Profile,
51 runtime_dir: PathBuf,
52 tag: Option<String>,
53 ) -> Result<Self, Error> {
54 let default_features = String::new();
55 let tag = match tag {
56 Some(tag) => tag,
57 _ => pop_common::docker::fetch_image_tag(SRTOOL_TAG_URL).await?,
58 };
59 let digest = Docker::get_image_digest(DEFAULT_IMAGE, &tag).await?;
60 let dir = fs::canonicalize(path.unwrap_or_else(|| PathBuf::from("./")))?;
61 let tmpdir = env::temp_dir().join("cargo");
62
63 let cache_mount = format!("{}:/cargo-home", tmpdir.display());
64
65 Ok(Self {
66 cache_mount,
67 default_features,
68 digest,
69 image: DEFAULT_IMAGE.to_string(),
70 package: package.to_owned(),
71 path: dir,
72 profile,
73 runtime_dir,
74 tag,
75 })
76 }
77
78 pub fn build(&self) -> Result<PathBuf, Error> {
80 let args = self.build_args()?;
81 cmd("docker", args).stdout_null().stderr_null().run()?;
82 Ok(self.get_output_path())
83 }
84
85 fn build_args(&self) -> Result<Vec<String>, Error> {
87 let package = format!("PACKAGE={}", self.package);
88 let absolute_workspace_path = std::fs::canonicalize(
91 rustilities::manifest::find_workspace_manifest(std::env::current_dir()?)
92 .ok_or(anyhow::anyhow!("Pop cannot determine your workspace path"))?
93 .parent()
94 .expect("A workspace manifest is a file and hence always have a parent; qed;"),
95 )?;
96 let runtime_dir = self
97 .runtime_dir
98 .strip_prefix(absolute_workspace_path)
99 .unwrap_or(&self.runtime_dir);
100 let runtime_dir = format!("RUNTIME_DIR={}", runtime_dir.display());
101 let default_features = format!("DEFAULT_FEATURES={}", self.default_features);
102 let profile = match self.profile {
103 Profile::Debug => "PROFILE=dev".to_owned(),
104 _ => format!("PROFILE={}", self.profile),
105 };
106 let image_digest = format!("IMAGE={}", self.digest);
107 let volume = format!("{}:/build", self.path.display());
108 let image_tag = format!("{}:{}", self.image, self.tag);
109
110 let args = vec![
111 "run".to_owned(),
112 "--name".to_owned(),
113 "srtool".to_owned(),
114 "--rm".to_owned(),
115 "-e".to_owned(),
116 package,
117 "-e".to_owned(),
118 runtime_dir,
119 "-e".to_owned(),
120 default_features,
121 "-e".to_owned(),
122 profile,
123 "-e".to_owned(),
124 image_digest,
125 "-v".to_owned(),
126 volume,
127 "-v".to_owned(),
128 self.cache_mount.clone(),
129 image_tag,
130 "build".to_owned(),
131 "--app".to_owned(),
132 "--json".to_owned(),
133 ];
134
135 Ok(args)
136 }
137
138 fn get_output_path(&self) -> PathBuf {
140 let output_wasm = match self.profile {
141 Profile::Debug => "wasm",
142 _ => "compact.compressed.wasm",
143 };
144 self.runtime_dir
145 .join("target")
146 .join("srtool")
147 .join(self.profile.to_string())
148 .join("wbuild")
149 .join(&self.package)
150 .join(format!("{}.{}", self.package.replace("-", "_"), output_wasm))
151 }
152}
153
154pub fn is_supported(path: &Path) -> bool {
160 let manifest = match from_path(path) {
161 Ok(m) => m,
162 Err(_) => return false,
163 };
164 const DEPENDENCIES: [&str; 3] = ["frame-system", "frame-support", "substrate-wasm-builder"];
166 let has_dependencies = DEPENDENCIES.into_iter().any(|d| {
167 manifest.dependencies.contains_key(d) ||
168 manifest.workspace.as_ref().is_some_and(|w| w.dependencies.contains_key(d))
169 });
170 let has_features = manifest.features.contains_key("runtime-benchmarks") ||
171 manifest.features.contains_key("try-runtime");
172 has_dependencies && has_features
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use anyhow::Result;
179 use fs::write;
180 use pop_common::manifest::Dependency;
181 use tempfile::tempdir;
182
183 const SRTOOL_TAG: &str = "1.88.0";
184 const SRTOOL_DIGEST: &str =
185 "sha256:9902e50293f55fa34bc8d83916aad3fdf9ab3c74f2c0faee6dec8cc705a3a5d7";
186
187 #[tokio::test]
188 async fn srtool_builder_new_works() {
189 Docker::ensure_running().await.unwrap();
190 let srtool_builder = DeterministicBuilder::new(
191 None,
192 "parachain-template-runtime",
193 Profile::Release,
194 PathBuf::from("./runtime"),
195 Some(SRTOOL_TAG.to_owned()),
196 )
197 .await
198 .unwrap();
199 assert_eq!(
200 srtool_builder.cache_mount,
201 format!("{}:/cargo-home", env::temp_dir().join("cargo").display())
202 );
203 assert_eq!(srtool_builder.default_features, "");
204 assert_eq!(srtool_builder.digest, SRTOOL_DIGEST);
205 assert_eq!(srtool_builder.tag, SRTOOL_TAG);
206
207 assert_eq!(srtool_builder.image, DEFAULT_IMAGE);
208 assert_eq!(srtool_builder.package, "parachain-template-runtime");
209 assert_eq!(srtool_builder.path, fs::canonicalize(PathBuf::from("./")).unwrap());
210 assert_eq!(srtool_builder.profile, Profile::Release);
211 assert_eq!(srtool_builder.runtime_dir, PathBuf::from("./runtime"));
212 }
213
214 #[tokio::test]
215 async fn build_args_works() {
216 Docker::ensure_running().await.unwrap();
217
218 let temp_dir = tempdir().unwrap();
219 let path = temp_dir.path();
220 assert_eq!(
221 DeterministicBuilder::new(
222 Some(path.to_path_buf()),
223 "parachain-template-runtime",
224 Profile::Production,
225 PathBuf::from("./runtime"),
226 Some(SRTOOL_TAG.to_owned())
227 )
228 .await
229 .unwrap()
230 .build_args()
231 .unwrap(),
232 vec!(
233 "run",
234 "--name",
235 "srtool",
236 "--rm",
237 "-e",
238 "PACKAGE=parachain-template-runtime",
239 "-e",
240 "RUNTIME_DIR=./runtime",
241 "-e",
242 "DEFAULT_FEATURES=",
243 "-e",
244 "PROFILE=production",
245 "-e",
246 &format!("IMAGE={SRTOOL_DIGEST}"),
247 "-v",
248 &format!("{}:/build", fs::canonicalize(path).unwrap().display()),
249 "-v",
250 &format!("{}:/cargo-home", env::temp_dir().join("cargo").display()),
251 &format!("{DEFAULT_IMAGE}:{SRTOOL_TAG}"),
252 "build",
253 "--app",
254 "--json"
255 ),
256 );
257 }
258
259 #[tokio::test]
260 async fn get_output_path_works() -> Result<()> {
261 Docker::ensure_running().await?;
262 let srtool_builder = DeterministicBuilder::new(
263 None,
264 "template-runtime",
265 Profile::Debug,
266 PathBuf::from("./runtime-folder"),
267 None,
268 )
269 .await?;
270 assert_eq!(
271 srtool_builder.get_output_path().display().to_string(),
272 "./runtime-folder/target/srtool/debug/wbuild/template-runtime/template_runtime.wasm"
273 );
274 Ok(())
275 }
276
277 #[test]
278 fn is_supported_works() -> Result<()> {
279 let temp_dir = tempdir()?;
280 let path = temp_dir.path();
281
282 let name = "hello_world";
284 cmd("cargo", ["new", name]).dir(path).run()?;
285 assert!(!is_supported(&path.join(name)));
286
287 let mut manifest = from_path(&path.join(name))?;
289 manifest
290 .dependencies
291 .insert("substrate-wasm-builder".into(), Dependency::Simple("^0.14.0".into()));
292 manifest.features.insert("try-runtime".into(), vec![]);
293 let manifest = toml_edit::ser::to_string_pretty(&manifest)?;
294 write(path.join(name).join("Cargo.toml"), manifest)?;
295 assert!(is_supported(&path.join(name)));
296 Ok(())
297 }
298}