referrerpolicy=no-referrer-when-downgrade

node_template_release/
main.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
5
6// This program is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10
11// This program is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14// GNU General Public License for more details.
15
16// You should have received a copy of the GNU General Public License
17// along with this program. If not, see <https://www.gnu.org/licenses/>.
18
19use std::{
20	collections::HashMap,
21	ffi::OsString,
22	fs::{self, File, OpenOptions},
23	path::{Path, PathBuf},
24	process::Command,
25};
26
27use clap::Parser;
28use flate2::{write::GzEncoder, Compression};
29use fs_extra::dir::{self, CopyOptions};
30use glob;
31use itertools::Itertools;
32use tar;
33use tempfile;
34use toml_edit::{self, value, Array, Item, Table};
35
36const SUBSTRATE_GIT_URL: &str = "https://github.com/paritytech/polkadot-sdk.git";
37
38type CargoToml = toml_edit::Document;
39
40#[derive(Debug, PartialEq)]
41struct Dependency {
42	name: String,
43	version: Option<String>,
44	default_features: Option<bool>,
45}
46
47type Dependencies = HashMap<String, HashMap<String, Dependency>>;
48
49#[derive(Parser)]
50struct Options {
51	/// The path to the `node-template` source.
52	#[arg()]
53	node_template: PathBuf,
54	/// The path where to output the generated `tar.gz` file.
55	#[arg()]
56	output: PathBuf,
57}
58
59/// Copy the `node-template` to the given path.
60fn copy_node_template(node_template: &Path, node_template_folder: &OsString, dest_path: &Path) {
61	let options = CopyOptions::new();
62	dir::copy(node_template, dest_path, &options).expect("Copies node-template to tmp dir");
63
64	let dest_path = dest_path.join(node_template_folder);
65
66	dir::get_dir_content(dest_path.join("env-setup"))
67		.expect("`env-setup` directory should exist")
68		.files
69		.iter()
70		.for_each(|f| {
71			fs::copy(
72				f,
73				dest_path.join(PathBuf::from(f).file_name().expect("File has a file name.")),
74			)
75			.expect("Copying from `env-setup` directory works");
76		});
77	dir::remove(dest_path.join("env-setup")).expect("Deleting `env-setup works`");
78}
79
80/// Find all `Cargo.toml` files in the given path.
81fn find_cargo_tomls(path: &PathBuf) -> Vec<PathBuf> {
82	let path = format!("{}/**/Cargo.toml", path.display());
83
84	let glob = glob::glob(&path).expect("Generates globbing pattern");
85
86	let mut result = Vec::new();
87	glob.into_iter().for_each(|file| match file {
88		Ok(file) => result.push(file),
89		Err(e) => println!("{:?}", e),
90	});
91
92	if result.is_empty() {
93		panic!("Did not found any `Cargo.toml` files.");
94	}
95
96	result
97}
98
99/// Parse the given `Cargo.toml`.
100fn parse_cargo_toml(file: &Path) -> CargoToml {
101	fs::read_to_string(file)
102		.unwrap_or_else(|e| panic!("Failed to read `{}`: {}", file.display(), e))
103		.parse()
104		.unwrap_or_else(|e| panic!("Failed to parse `{}`: {}", file.display(), e))
105}
106
107/// Write the given `Cargo.toml` to the given path.
108fn write_cargo_toml(path: &Path, cargo_toml: CargoToml) {
109	fs::write(path, cargo_toml.to_string())
110		.unwrap_or_else(|e| panic!("Failed to write `{}`: {}", path.display(), e));
111}
112
113/// Gets the latest commit id of the repository given by `path`.
114fn get_git_commit_id(path: &Path) -> String {
115	let mut dir = path;
116	while !dir.join(".git").exists() {
117		dir = dir
118			.parent()
119			.expect(&format!("Node template ({}) should be in a git repository.", path.display()));
120	}
121
122	let git = dir.join(".git");
123	let head = git.join("HEAD");
124	let head_contents = fs::read_to_string(head).expect("Repository should have a HEAD");
125	let branch = head_contents.strip_prefix("ref: ").expect(".git/HEAD to start 'ref: '").trim();
126	let mut commit = fs::read_to_string(git.join(branch)).expect("Head references a commit");
127	commit.truncate(commit.trim_end().len());
128	commit
129}
130
131/// Rewrites git dependencies:
132/// - inserts `workspace = true`;
133/// - removes `path`;
134/// - removes `version`;
135/// - removes `default-features`
136/// - and returns the dependencies that were rewritten.
137fn update_git_dependencies<F: Copy + Fn(&str) -> bool>(
138	cargo_toml: &mut CargoToml,
139	path_filter: F,
140) -> Dependencies {
141	let process_dep = |dep: (toml_edit::KeyMut, &mut Item)| -> Option<Dependency> {
142		let (key, value) = dep;
143		value
144			.as_table_like_mut()
145			.filter(|dep| {
146				dep.get("path").and_then(|path| path.as_str()).map(path_filter).unwrap_or(false)
147			})
148			.map(|dep| {
149				dep.insert("workspace", toml_edit::value(true));
150				dep.remove("path");
151
152				Dependency {
153					name: key.get().to_string(),
154					version: dep
155						.remove("version")
156						.and_then(|version| version.as_str().map(|s| s.to_string())),
157					default_features: dep.remove("default-features").and_then(|b| b.as_bool()),
158				}
159			})
160	};
161
162	["dependencies", "build-dependencies", "dev-dependencies"]
163		.into_iter()
164		.map(|table| -> (String, HashMap<String, Dependency>) {
165			(
166				table.to_string(),
167				cargo_toml[table]
168					.as_table_mut()
169					.into_iter()
170					.flat_map(|deps| deps.iter_mut().filter_map(process_dep))
171					.map(|dep| (dep.name.clone(), dep))
172					.collect(),
173			)
174		})
175		.collect()
176}
177
178/// Processes all `Cargo.toml` files, aggregates dependencies and saves the changes.
179fn process_cargo_tomls(cargo_tomls: &Vec<PathBuf>) -> Dependencies {
180	/// Merge dependencies from one collection in another.
181	fn merge_deps(into: &mut Dependencies, from: Dependencies) {
182		from.into_iter().for_each(|(table, deps)| {
183			into.entry(table).or_insert_with(HashMap::new).extend(deps);
184		});
185	}
186
187	cargo_tomls.iter().fold(Dependencies::new(), |mut acc, path| {
188		let mut cargo_toml = parse_cargo_toml(&path);
189
190		let mut cargo_toml_path = path.clone();
191		cargo_toml_path.pop(); // remove `Cargo.toml` from the path
192		let deps = update_git_dependencies(&mut cargo_toml, |dep_path| {
193			!cargo_toml_path.join(dep_path).exists()
194		});
195
196		write_cargo_toml(&path, cargo_toml);
197		merge_deps(&mut acc, deps);
198		acc
199	})
200}
201
202/// Update the top level (workspace) `Cargo.toml` file.
203///
204/// - Adds `workspace` definition
205/// - Adds dependencies
206/// - Adds `profile.release` = `panic = unwind`
207fn update_root_cargo_toml(
208	cargo_toml: &mut CargoToml,
209	members: &[String],
210	deps: Dependencies,
211	commit_id: &str,
212) {
213	let mut workspace = Table::new();
214
215	workspace.insert("resolver", value("2"));
216
217	workspace.insert("members", value(Array::from_iter(members.iter())));
218	let mut workspace_dependencies = Table::new();
219	deps.values()
220		.flatten()
221		.sorted_by_key(|(name, _)| *name)
222		.for_each(|(name, dep)| {
223			if let Some(version) = &dep.version {
224				workspace_dependencies[name]["version"] = value(version);
225			}
226			if let Some(default_features) = dep.default_features {
227				workspace_dependencies[name]["default-features"] = value(default_features);
228			}
229			workspace_dependencies[name]["git"] = value(SUBSTRATE_GIT_URL);
230			workspace_dependencies[name]["rev"] = value(commit_id);
231		});
232
233	let mut package = Table::new();
234	package.insert("edition", value("2021"));
235	workspace.insert("package", Item::Table(package));
236
237	workspace.insert("dependencies", Item::Table(workspace_dependencies));
238
239	workspace.insert("lints", Item::Table(Table::new()));
240	cargo_toml.insert("workspace", Item::Table(workspace));
241
242	let mut panic_unwind = Table::new();
243	panic_unwind.insert("panic", value("unwind"));
244	let mut profile = Table::new();
245	profile.insert("release", Item::Table(panic_unwind));
246	cargo_toml.insert("profile", Item::Table(profile.into()));
247}
248
249fn process_root_cargo_toml(
250	root_cargo_toml_path: &Path,
251	root_deps: Dependencies,
252	cargo_tomls: &[PathBuf],
253	node_template_path: &PathBuf,
254	commit_id: &str,
255) {
256	let mut root_cargo_toml = parse_cargo_toml(root_cargo_toml_path);
257	let workspace_members = cargo_tomls
258		.iter()
259		.map(|p| {
260			p.strip_prefix(node_template_path)
261				.expect("Workspace member is a child of the node template path!")
262				.parent()
263				// We get the `Cargo.toml` paths as workspace members, but for the `members` field
264				// we just need the path.
265				.expect("The given path ends with `Cargo.toml` as file name!")
266				.display()
267				.to_string()
268		})
269		.collect::<Vec<_>>();
270
271	update_root_cargo_toml(&mut root_cargo_toml, &workspace_members, root_deps, commit_id);
272	write_cargo_toml(&root_cargo_toml_path, root_cargo_toml);
273}
274
275/// Build and test the generated node-template
276fn build_and_test(path: &Path, cargo_tomls: &[PathBuf]) {
277	// Build node
278	assert!(Command::new("cargo")
279		.args(&["build", "--all"])
280		.current_dir(path)
281		.status()
282		.expect("Compiles node")
283		.success());
284
285	// Test node
286	assert!(Command::new("cargo")
287		.args(&["test", "--all"])
288		.current_dir(path)
289		.status()
290		.expect("Tests node")
291		.success());
292
293	// Remove all `target` directories
294	for toml in cargo_tomls {
295		let mut target_path = toml.clone();
296		target_path.pop();
297		target_path = target_path.join("target");
298
299		if target_path.exists() {
300			fs::remove_dir_all(&target_path)
301				.expect(&format!("Removes `{}`", target_path.display()));
302		}
303	}
304}
305
306fn main() {
307	let options = Options::parse();
308
309	// Copy node-template to a temp build dir.
310	let build_dir = tempfile::tempdir().expect("Creates temp build dir");
311	let node_template_folder = options
312		.node_template
313		.canonicalize()
314		.expect("Node template path exists")
315		.file_name()
316		.expect("Node template folder is last element of path")
317		.to_owned();
318	copy_node_template(&options.node_template, &node_template_folder, build_dir.path());
319
320	// The path to the node-template in the build dir.
321	let node_template_path = build_dir.path().join(node_template_folder);
322	let root_cargo_toml_path = node_template_path.join("Cargo.toml");
323
324	// Get all `Cargo.toml` files in the node-template
325	let mut cargo_tomls = find_cargo_tomls(&node_template_path);
326
327	// Check if top level Cargo.toml exists. If not, create one in the destination,
328	// else remove it from the list, as this requires some special treatments.
329	if let Some(index) = cargo_tomls.iter().position(|x| *x == root_cargo_toml_path) {
330		cargo_tomls.remove(index);
331	} else {
332		OpenOptions::new()
333			.create(true)
334			.write(true)
335			.open(root_cargo_toml_path.clone())
336			.expect("Create root level `Cargo.toml` failed.");
337	}
338
339	// Process all `Cargo.toml` files.
340	let root_deps = process_cargo_tomls(&cargo_tomls);
341	process_root_cargo_toml(
342		&root_cargo_toml_path,
343		root_deps,
344		&cargo_tomls,
345		&node_template_path,
346		&get_git_commit_id(&options.node_template),
347	);
348
349	// Add root rustfmt to node template build path.
350	let node_template_rustfmt_toml_path = node_template_path.join("rustfmt.toml");
351	let root_rustfmt_toml = &options.node_template.join("../../rustfmt.toml");
352	if root_rustfmt_toml.exists() {
353		fs::copy(&root_rustfmt_toml, &node_template_rustfmt_toml_path)
354			.expect("Copying rustfmt.toml.");
355	}
356
357	build_and_test(&node_template_path, &cargo_tomls);
358
359	let output = GzEncoder::new(
360		File::create(&options.output).expect("Creates output file"),
361		Compression::default(),
362	);
363	let mut tar = tar::Builder::new(output);
364	tar.append_dir_all("substrate-node-template", node_template_path)
365		.expect("Writes substrate-node-template archive");
366}
367
368#[cfg(test)]
369mod tests {
370	use super::*;
371
372	#[test]
373	fn test_update_git_dependencies() {
374		let toml = r#"
375[dev-dependencies]
376scale-info = { version = "2.5.0", default-features = false, features = ["derive"] }
377
378[dependencies]
379scale-info = { version = "2.5.0", default-features = false, features = ["derive"] }
380sp-io = { version = "7.0.0", path = "../../../../primitives/io" }
381frame-system = { version = "4.0.0-dev", default-features = false, path = "../../../../frame/system" }
382"#;
383		let mut cargo_toml = toml.parse::<CargoToml>().expect("invalid doc");
384		let actual_deps = update_git_dependencies(&mut cargo_toml, |_| true);
385
386		assert_eq!(actual_deps.len(), 3);
387		assert_eq!(actual_deps.get("dependencies").unwrap().len(), 2);
388		assert_eq!(actual_deps.get("dev-dependencies").unwrap().len(), 0);
389		assert_eq!(
390			actual_deps.get("dependencies").unwrap().get("sp-io").unwrap(),
391			&Dependency {
392				name: "sp-io".into(),
393				version: Some("7.0.0".into()),
394				default_features: None
395			}
396		);
397		assert_eq!(
398			actual_deps.get("dependencies").unwrap().get("frame-system").unwrap(),
399			&Dependency {
400				name: "frame-system".into(),
401				version: Some("4.0.0-dev".into()),
402				default_features: Some(false),
403			}
404		);
405
406		let expected_toml = r#"
407[dev-dependencies]
408scale-info = { version = "2.5.0", default-features = false, features = ["derive"] }
409
410[dependencies]
411scale-info = { version = "2.5.0", default-features = false, features = ["derive"] }
412sp-io = { workspace = true }
413frame-system = { workspace = true }
414"#;
415		assert_eq!(cargo_toml.to_string(), expected_toml);
416	}
417
418	#[test]
419	fn test_update_root_cargo_toml() {
420		let mut cargo_toml = CargoToml::new();
421		update_root_cargo_toml(
422			&mut cargo_toml,
423			&vec!["node".into(), "pallets/template".into(), "runtime".into()],
424			Dependencies::from([
425				(
426					"dependencies".into(),
427					HashMap::from([
428						(
429							"sp-io".into(),
430							Dependency {
431								name: "sp-io".into(),
432								version: Some("7.0.0".into()),
433								default_features: None,
434							},
435						),
436						(
437							"frame-system".into(),
438							Dependency {
439								name: "frame-system".into(),
440								version: Some("4.0.0-dev".into()),
441								default_features: Some(true),
442							},
443						),
444					]),
445				),
446				("dev-dependencies".into(), HashMap::new()),
447				("build-dependencies".into(), HashMap::new()),
448			]),
449			"commit_id",
450		);
451
452		let expected_toml = r#"[workspace]
453resolver = "2"
454members = ["node", "pallets/template", "runtime"]
455
456[workspace.package]
457edition = "2021"
458
459[workspace.dependencies]
460frame-system = { version = "4.0.0-dev", default-features = true, git = "https://github.com/paritytech/polkadot-sdk.git", rev = "commit_id" }
461sp-io = { version = "7.0.0", git = "https://github.com/paritytech/polkadot-sdk.git", rev = "commit_id" }
462
463[workspace.lints]
464
465[profile]
466
467[profile.release]
468panic = "unwind"
469"#;
470		assert_eq!(cargo_toml.to_string(), expected_toml);
471	}
472}