Skip to main content

pop_chains/build/
runtime.rs

1// SPDX-License-Identifier: GPL-3.0
2
3use 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
15/// Builds and executes the command for running a deterministic runtime build process using
16/// srtool.
17pub struct DeterministicBuilder {
18	/// Mount point for cargo cache.
19	cache_mount: String,
20	/// List of default features to enable during the build process.
21	default_features: String,
22	/// Digest of the image for reproducibility.
23	digest: String,
24	/// Name of the image used for building.
25	image: String,
26	/// The runtime package name.
27	package: String,
28	/// The path to the project directory.
29	path: PathBuf,
30	/// The profile used for building.
31	profile: Profile,
32	/// The directory path where the runtime is located.
33	runtime_dir: PathBuf,
34	/// The tag of the image to use.
35	tag: String,
36}
37
38impl DeterministicBuilder {
39	/// Creates a new instance of `Builder`.
40	///
41	/// # Arguments
42	/// * `path` - The path to the project.
43	/// * `package` - The runtime package name.
44	/// * `profile` - The profile to build the runtime.
45	/// * `runtime_dir` - The directory path where the runtime is located.
46	/// * `tag` - The tag of the srtool image to be used
47	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	/// Executes the runtime build process and returns the path of the generated file.
79	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	// Builds the srtool runtime container command string.
86	fn build_args(&self) -> Result<Vec<String>, Error> {
87		let package = format!("PACKAGE={}", self.package);
88		// Runtime dir might be the absolute path to the runtime dir if the user didn't specified
89		// it. This causes docker runs to fail, so we need to strip the prefix.
90		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	// Returns the expected output path of the compiled runtime `.wasm` file.
139	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
154/// Determines whether the manifest at the supplied path is a supported Substrate runtime project.
155///
156/// # Arguments
157/// * `path` - The optional path to the manifest, defaulting to the current directory if not
158///   specified.
159pub fn is_supported(path: &Path) -> bool {
160	let manifest = match from_path(path) {
161		Ok(m) => m,
162		Err(_) => return false,
163	};
164	// Simply check for a parachain dependency
165	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		// Standard rust project
283		let name = "hello_world";
284		cmd("cargo", ["new", name]).dir(path).run()?;
285		assert!(!is_supported(&path.join(name)));
286
287		// Parachain runtime with dependency
288		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}