pop_chains/build/
runtime.rs

1// SPDX-License-Identifier: GPL-3.0
2
3use crate::Error;
4use duct::cmd;
5use pop_common::{Profile, manifest::from_path};
6pub use srtool_lib::{ContainerEngine, get_image_digest, get_image_tag};
7use std::{
8	env, fs,
9	path::{Path, PathBuf},
10};
11
12const DEFAULT_IMAGE: &str = "docker.io/paritytech/srtool";
13const TIMEOUT: u64 = 60 * 60;
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	/// The container engine used to run the build process.
25	engine: ContainerEngine,
26	/// Name of the image used for building.
27	image: String,
28	/// The runtime package name.
29	package: String,
30	/// The path to the project directory.
31	path: PathBuf,
32	/// The profile used for building.
33	profile: Profile,
34	/// The directory path where the runtime is located.
35	runtime_dir: PathBuf,
36	/// The tag of the image to use.
37	tag: String,
38}
39
40impl DeterministicBuilder {
41	/// Creates a new instance of `Builder`.
42	///
43	/// # Arguments
44	/// * `engine` - The container engine to use.
45	/// * `path` - The path to the project.
46	/// * `package` - The runtime package name.
47	/// * `profile` - The profile to build the runtime.
48	/// * `runtime_dir` - The directory path where the runtime is located.
49	pub fn new(
50		engine: ContainerEngine,
51		path: Option<PathBuf>,
52		package: &str,
53		profile: Profile,
54		runtime_dir: PathBuf,
55	) -> Result<Self, Error> {
56		let default_features = String::new();
57		let tag = get_image_tag(Some(TIMEOUT)).map_err(|_| Error::ImageTagRetrievalFailed)?;
58		let digest = get_image_digest(DEFAULT_IMAGE, &tag).unwrap_or_default();
59		let dir = fs::canonicalize(path.unwrap_or_else(|| PathBuf::from("./")))?;
60		let tmpdir = env::temp_dir().join("cargo");
61
62		let no_cache = engine == ContainerEngine::Podman;
63		let cache_mount =
64			if !no_cache { format!("-v {}:/cargo-home", tmpdir.display()) } else { String::new() };
65
66		Ok(Self {
67			cache_mount,
68			default_features,
69			digest,
70			engine,
71			image: DEFAULT_IMAGE.to_string(),
72			package: package.to_owned(),
73			path: dir,
74			profile,
75			runtime_dir,
76			tag,
77		})
78	}
79
80	/// Executes the runtime build process and returns the path of the generated file.
81	pub fn build(&self) -> Result<PathBuf, Error> {
82		let command = self.build_command();
83		cmd("sh", vec!["-c", &command]).stdout_null().stderr_null().run()?;
84		let wasm_path = self.get_output_path();
85		Ok(wasm_path)
86	}
87
88	// Builds the srtool runtime container command string.
89	fn build_command(&self) -> String {
90		format!(
91			"{} run --name srtool --rm \
92			 -e PACKAGE={} \
93			 -e RUNTIME_DIR={} \
94			 -e DEFAULT_FEATURES={} \
95			 -e PROFILE={} \
96			 -e IMAGE={} \
97			 -v {}:/build \
98			 {} \
99			 {}:{} build --app --json",
100			self.engine,
101			self.package,
102			self.runtime_dir.display(),
103			self.default_features,
104			self.profile,
105			self.digest,
106			self.path.display(),
107			self.cache_mount,
108			self.image,
109			self.tag
110		)
111	}
112
113	// Returns the expected output path of the compiled runtime `.wasm` file.
114	fn get_output_path(&self) -> PathBuf {
115		self.runtime_dir
116			.join("target")
117			.join("srtool")
118			.join(self.profile.to_string())
119			.join("wbuild")
120			.join(&self.package)
121			.join(format!("{}.compact.compressed.wasm", self.package.replace("-", "_")))
122	}
123}
124
125/// Determines whether the manifest at the supplied path is a supported Substrate runtime project.
126///
127/// # Arguments
128/// * `path` - The optional path to the manifest, defaulting to the current directory if not
129///   specified.
130pub fn is_supported(path: &Path) -> bool {
131	let manifest = match from_path(path) {
132		Ok(m) => m,
133		Err(_) => return false,
134	};
135	// Simply check for a parachain dependency
136	const DEPENDENCIES: [&str; 3] = ["frame-system", "frame-support", "substrate-wasm-builder"];
137	let has_dependencies = DEPENDENCIES.into_iter().any(|d| {
138		manifest.dependencies.contains_key(d) ||
139			manifest.workspace.as_ref().is_some_and(|w| w.dependencies.contains_key(d))
140	});
141	let has_features = manifest.features.contains_key("runtime-benchmarks") ||
142		manifest.features.contains_key("try-runtime");
143	has_dependencies && has_features
144}
145
146#[cfg(test)]
147mod tests {
148	use super::*;
149	use anyhow::Result;
150	use fs::write;
151	use pop_common::manifest::Dependency;
152	use tempfile::tempdir;
153
154	#[test]
155	fn srtool_builder_new_works() -> Result<()> {
156		let srtool_builer = DeterministicBuilder::new(
157			ContainerEngine::Docker,
158			None,
159			"parachain-template-runtime",
160			Profile::Release,
161			PathBuf::from("./runtime"),
162		)?;
163		assert_eq!(
164			srtool_builer.cache_mount,
165			format!("-v {}:/cargo-home", env::temp_dir().join("cargo").display())
166		);
167		assert_eq!(srtool_builer.default_features, "");
168
169		let tag = get_image_tag(Some(TIMEOUT))?;
170		let digest = get_image_digest(DEFAULT_IMAGE, &tag).unwrap_or_default();
171		assert_eq!(srtool_builer.digest, digest);
172		assert_eq!(srtool_builer.tag, tag);
173
174		assert!(srtool_builer.engine == ContainerEngine::Docker);
175		assert_eq!(srtool_builer.image, DEFAULT_IMAGE);
176		assert_eq!(srtool_builer.package, "parachain-template-runtime");
177		assert_eq!(srtool_builer.path, fs::canonicalize(PathBuf::from("./"))?);
178		assert_eq!(srtool_builer.profile, Profile::Release);
179		assert_eq!(srtool_builer.runtime_dir, PathBuf::from("./runtime"));
180
181		Ok(())
182	}
183
184	#[test]
185	fn build_command_works() -> Result<()> {
186		let temp_dir = tempdir()?;
187		let path = temp_dir.path();
188		let tag = get_image_tag(Some(TIMEOUT))?;
189		let digest = get_image_digest(DEFAULT_IMAGE, &tag).unwrap_or_default();
190		assert_eq!(
191			DeterministicBuilder::new(
192				ContainerEngine::Podman,
193				Some(path.to_path_buf()),
194				"parachain-template-runtime",
195				Profile::Production,
196				PathBuf::from("./runtime"),
197			)?
198			.build_command(),
199			format!(
200				"podman run --name srtool --rm \
201			 -e PACKAGE=parachain-template-runtime \
202			 -e RUNTIME_DIR=./runtime \
203			 -e DEFAULT_FEATURES= \
204			 -e PROFILE=production \
205			 -e IMAGE={} \
206			 -v {}:/build \
207			 {} \
208			 {}:{} build --app --json",
209				digest,
210				fs::canonicalize(path)?.display(),
211				String::new(),
212				DEFAULT_IMAGE,
213				tag
214			)
215		);
216		Ok(())
217	}
218
219	#[test]
220	fn get_output_path_works() -> Result<()> {
221		let srtool_builder = DeterministicBuilder::new(
222			ContainerEngine::Podman,
223			None,
224			"template-runtime",
225			Profile::Debug,
226			PathBuf::from("./runtime-folder"),
227		)?;
228		assert_eq!(
229			srtool_builder.get_output_path().display().to_string(),
230			"./runtime-folder/target/srtool/debug/wbuild/template-runtime/template_runtime.compact.compressed.wasm"
231		);
232		Ok(())
233	}
234
235	#[test]
236	fn is_supported_works() -> Result<()> {
237		let temp_dir = tempdir()?;
238		let path = temp_dir.path();
239
240		// Standard rust project
241		let name = "hello_world";
242		cmd("cargo", ["new", name]).dir(path).run()?;
243		assert!(!is_supported(&path.join(name)));
244
245		// Parachain runtime with dependency
246		let mut manifest = from_path(&path.join(name))?;
247		manifest
248			.dependencies
249			.insert("substrate-wasm-builder".into(), Dependency::Simple("^0.14.0".into()));
250		manifest.features.insert("try-runtime".into(), vec![]);
251		let manifest = toml_edit::ser::to_string_pretty(&manifest)?;
252		write(path.join(name).join("Cargo.toml"), manifest)?;
253		assert!(is_supported(&path.join(name)));
254		Ok(())
255	}
256}