""" Creates the Pezkuwi-SDK umbrella crate that re-exports all other crates. This re-creates the `umbrella/` folder. Ensure that it does not contain any changes you want to keep. Usage: python3 generate-umbrella.py --sdk --version Example: python3 generate-umbrella.py --sdk .. --version 1.11.0 """ import argparse import os import re import toml import shutil import subprocess from cargo_workspace import Workspace """ Crate names that should be excluded from the umbrella crate. """ def exclude(crate): name = crate.name if crate.metadata.get("pezkuwi-sdk.exclude-from-umbrella", False): return True # No fuzzers or examples: if "example" in name or name.endswith("fuzzer"): return True # No runtime crates: if name.endswith("-runtime"): # Note: this is a bit hacky. We should use custom crate metadata instead. return name != "sp-runtime" and name != "pezbp-runtime" and name != "frame-try-runtime" # Exclude snowbridge crates. if name.startswith("snowbridge-"): return True return False def main(path, version): delete_umbrella(path) workspace = Workspace.from_path(path) print(f'Indexed {workspace}') std_crates = [] # name -> path. use list for sorting nostd_crates = [] for crate in workspace.crates: if crate.name == 'pezkuwi-sdk': continue if not crate.publish: print(f"Skipping {crate.name} as it is not published") continue lib_path = os.path.dirname(crate.abs_path) manifest_path = os.path.join(lib_path, "Cargo.toml") lib_path = os.path.join(lib_path, "src", "lib.rs") path = os.path.dirname(crate.rel_path) # Guess which crates support no_std. Proc-macro crates are always no_std: with open(manifest_path, "r") as f: manifest = toml.load(f) if 'lib' in manifest and 'proc-macro' in manifest['lib']: if manifest['lib']['proc-macro']: nostd_crates.append((crate, path)) continue # Crates without a lib.rs cannot be no_std if not os.path.exists(lib_path): print(f"Skipping {crate.name} as it does not have a 'src/lib.rs'") continue if exclude(crate): print(f"Skipping {crate.name} as it is in the exclude list") continue # No search for a no_std attribute: with open(lib_path, "r") as f: nostd_crate = False for line in f: line = line.strip() if line == "#![no_std]" or line == '#![cfg_attr(not(feature = "std"), no_std)]': nostd_crate = True break elif "no_std" in line: print(line) if nostd_crate: nostd_crates.append((crate, path)) else: std_crates.append((crate, path)) # Sort by name std_crates.sort(key=lambda x: x[0].name) nostd_crates.sort(key=lambda x: x[0].name) # Client-side crates that declare no_std but have std-only transitive # dependencies (e.g. thiserror v1, regex). Exclude from runtime-full # to prevent wasm32v1-none compilation failures. RUNTIME_FULL_EXCLUDE = { "pezkuwi-subxt-signer", "pezkuwi-subxt-core", "pezkuwi-subxt-macro", "pezkuwi-subxt-metadata", } runtime_crates = [crate for crate in nostd_crates if 'frame' in crate[0].name or crate[0].name.startswith('sp-')] all_crates = std_crates + nostd_crates all_crates.sort(key=lambda x: x[0].name) dependencies = {} for (crate, path) in nostd_crates: dependencies[crate.name] = {"path": f"../{path}", "default-features": False, "optional": True} for (crate, path) in std_crates: dependencies[crate.name] = {"path": f"../{path}", "default-features": False, "optional": True} # The empty features are filled by Zepter features = { "default": [ "std" ], "std": [], "runtime-benchmarks": [], "try-runtime": [], "serde": [], "experimental": [], "with-tracing": [], "runtime-full": list([f"{d.name}" for d, _ in nostd_crates if d.name not in RUNTIME_FULL_EXCLUDE]), "runtime": list([f"{d.name}" for d, _ in runtime_crates]), "node": ["std"] + list([f"{d.name}" for d, _ in std_crates]), "tuples-96": [], } manifest = { "package": { "name": "pezkuwi-sdk", "version": version, "edition": { "workspace": True }, "authors": { "workspace": True }, "description": "Pezkuwi SDK umbrella crate.", "homepage": { "workspace": True }, "repository": { "workspace": True }, "license": "Apache-2.0", "metadata": { "docs": { "rs": { "features": ["runtime-full", "node"], "targets": ["x86_64-unknown-linux-gnu"] }}} }, "dependencies": dependencies, "features": features, } umbrella_dir = os.path.join(workspace.path, "umbrella") manifest_path = os.path.join(umbrella_dir, "Cargo.toml") lib_path = os.path.join(umbrella_dir, "src", "lib.rs") # create all dir os.makedirs(os.path.dirname(lib_path), exist_ok=True) # Write the manifest with open(manifest_path, "w") as f: toml_manifest = toml.dumps(manifest) f.write(toml_manifest) print(f"Wrote {manifest_path}") # Format with taplo to match CI expectations taplo_config = os.path.join(workspace.path, ".config", "taplo.toml") if os.path.exists(taplo_config): try: subprocess.run(["taplo", "format", "--config", taplo_config, manifest_path], check=True, capture_output=True) print(f"Formatted {manifest_path} with taplo") except (subprocess.CalledProcessError, FileNotFoundError) as e: print(f"Warning: Could not format with taplo: {e}") # and the lib.rs with open(lib_path, "w") as f: f.write('''// Copyright (C) Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 #![cfg_attr(not(feature = "std"), no_std)] //! Pezkuwi SDK umbrella crate re-exporting all other published crates. //! //! This helps to set a single version number for all your dependencies. Docs are in the //! `pezkuwi-sdk-docs` crate. // This file is auto-generated and checked by the CI. You can edit it manually, but it must be // exactly the way that the CI expects it. ''') for crate, _ in all_crates: use = crate.name.replace("-", "_") desc = crate.description if crate.description.endswith(".") else crate.description + "." f.write(f'\n/// {desc}') f.write(f'\n#[cfg(feature = "{crate.name}")]\n') f.write(f"pub use {use};\n") print(f"Wrote {lib_path}") add_to_workspace(workspace.path) """ Delete the umbrella folder and remove the umbrella crate from the workspace. """ def delete_umbrella(path): # remove the umbrella crate from the workspace manifest = os.path.join(path, "Cargo.toml") manifest = open(manifest, "r").read() manifest = re.sub(r'\s+"umbrella",\n', "", manifest) with open(os.path.join(path, "Cargo.toml"), "w") as f: f.write(manifest) umbrella_dir = os.path.join(path, "umbrella") if os.path.exists(umbrella_dir): print(f"Deleting {umbrella_dir}") cargo_toml = os.path.join(umbrella_dir, "Cargo.toml") if os.path.exists(cargo_toml): os.remove(cargo_toml) src_dir = os.path.join(umbrella_dir, "src") if os.path.exists(src_dir): shutil.rmtree(src_dir) """ Create the umbrella crate and add it to the workspace. """ def add_to_workspace(path): manifest = os.path.join(path, "Cargo.toml") manifest = open(manifest, "r").read() manifest = re.sub(r'^members = \[', 'members = [\n "umbrella",', manifest, flags=re.M) with open(os.path.join(path, "Cargo.toml"), "w") as f: f.write(manifest) os.chdir(path) # hack os.system("cargo metadata --format-version 1 > /dev/null") # update the lockfile os.system(f"zepter") # enable the features os.system(f"taplo format --config .config/taplo.toml Cargo.toml umbrella/Cargo.toml") os.system(f"cargo fmt -- umbrella/src/lib.rs") # format lib.rs for rustfmt compliance def parse_args(): parser = argparse.ArgumentParser(description="Create a pezkuwi-sdk crate") parser.add_argument("--sdk", type=str, default="pezkuwi-sdk", help="Path to the pezkuwi-sdk crate") parser.add_argument("--version", type=str, help="Version of the pezkuwi-sdk crate") return parser.parse_args() if __name__ == "__main__": args = parse_args() main(args.sdk, args.version)