1use 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 #[arg()]
53 node_template: PathBuf,
54 #[arg()]
56 output: PathBuf,
57}
58
59fn 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
80fn 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
99fn 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
107fn 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
113fn 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
131fn 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
178fn process_cargo_tomls(cargo_tomls: &Vec<PathBuf>) -> Dependencies {
180 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(); 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
202fn 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 .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
275fn build_and_test(path: &Path, cargo_tomls: &[PathBuf]) {
277 assert!(Command::new("cargo")
279 .args(&["build", "--all"])
280 .current_dir(path)
281 .status()
282 .expect("Compiles node")
283 .success());
284
285 assert!(Command::new("cargo")
287 .args(&["test", "--all"])
288 .current_dir(path)
289 .status()
290 .expect("Tests node")
291 .success());
292
293 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 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 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 let mut cargo_tomls = find_cargo_tomls(&node_template_path);
326
327 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 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 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}