UNPKG

sindri

Version:

The Sindri Labs JavaScript SDK and CLI tool.

480 lines (468 loc) 16.1 kB
import { execSync } from "child_process"; import { existsSync, mkdirSync, readdirSync, rmSync, statSync } from "fs"; import path from "path"; import process from "process"; import { Command } from "@commander-js/extra-typings"; import { confirm, input, select } from "@inquirer/prompts"; import { scaffoldDirectory } from "cli/utils"; import sindri from "lib"; export const initCommand = new Command() .name("init") .description("Initialize a new Sindri project.") .argument( "[directory]", "The directory where the new project should be initialized.", ".", ) .action(async (directory) => { // Prepare the directory paths. const directoryPath = path.resolve(directory); const directoryName = path.basename(directoryPath); // Ensure that the directory exists. if (!existsSync(directoryPath)) { mkdirSync(directoryPath, { recursive: true }); } else if (!statSync(directoryPath).isDirectory()) { sindri.logger.warn( `File "${directoryPath}" exists and is not a directory, aborting.`, ); return process.exit(1); } // Check that the directory is empty. const existingFiles = readdirSync(directoryPath); if (existingFiles.length > 0) { const proceed = await confirm({ message: `The "${directoryPath}" directory already exists and contains files. Continuing will ` + "overwrite your existing files. Are you *SURE* you would like to proceed?", default: false, }); if (!proceed) { sindri.logger.info("Aborting."); return process.exit(1); } } // Collect common fields. const circuitName = await input({ message: "Circuit Name:", default: directoryName.replace(/[^-a-zA-Z0-9_]/g, "-"), validate: (input): boolean | string => { if (input.length === 0) { return "You must specify a circuit name."; } if (!/^[-a-zA-Z0-9_]+$/.test(input)) { return "Only alphanumeric characters, hyphens, and underscores are allowed."; } return true; }, }); const circuitType: | "circom" | "gnark" | "halo2" | "jolt" | "noir" | "plonky2" | "sp1" = await select({ message: "Proving Framework:", default: "circom", choices: [ { name: "Circom", value: "circom" }, { name: "Gnark", value: "gnark" }, { name: "Halo2", value: "halo2" }, { name: "Jolt", value: "jolt" }, { name: "Noir", value: "noir" }, { name: "Plonky2", value: "plonky2" }, { name: "SP1", value: "sp1" }, ], }); const context: object = { circuitName, circuitType }; let templateDirectory: string = circuitType; // Handle individual circuit types. if (circuitType === "circom") { // Circom. const provingScheme: "groth16" = await select({ message: "Proving Scheme:", default: "groth16", choices: [{ name: "Groth16", value: "groth16" }], }); const curveName: "bn254" = await select({ message: "Curve Name:", default: "bn254", choices: [{ name: "BN254", value: "bn254" }], }); const witnessCompiler: "c++" | "wasm" = await select({ message: "Witness Compiler:", default: "c++", choices: [ { name: "C++", value: "c++" }, { name: "Wasm", value: "wasm" }, ], }); Object.assign(context, { curveName, provingScheme, witnessCompiler, }); } else if (circuitType === "gnark") { // Gnark. const packageName = await input({ message: "Go Package Name:", default: circuitName .replace(/[^a-zA-Z0-9]/g, "") .replace(/^[^a-z]*/g, ""), validate: (input): boolean | string => { if (input.length === 0) { return "You must specify a package name."; } if (!/^[a-z][a-z0-9]*$/.test(input)) { return ( "Package names must begin with a lowercase letter and only be followed by " + "alphanumeric characters." ); } return true; }, }); const provingScheme: "groth16" | "plonk" = await select({ message: "Proving Scheme:", default: "groth16", choices: [ { name: "Groth16", value: "groth16" }, { name: "PlonK", value: "plonk" }, ], }); const curveName: | "bn254" | "bls12-377" | "bls12-381" | "bls24-315" | "bw6-633" | "bw6-761" = await select({ message: "Curve Name:", default: "bn254", choices: [ { name: "BN254", value: "bn254" }, { name: "BLS12-377", value: "bls12-377" }, { name: "BLS12-381", value: "bls12-381" }, { name: "BLS24-315", value: "bls24-315" }, { name: "BW6-633", value: "bw6-633" }, { name: "BW6-761", value: "bw6-761" }, ], }); const gnarkCurveName = curveName.toUpperCase().replace("-", "_"); Object.assign(context, { curveName, gnarkCurveName, packageName, provingScheme, }); } else if (circuitType === "halo2") { // Halo2. const halo2Version: "axiom-v0.3.0" | "pse-v0.3.0" = await select({ message: "Halo2 Base Version:", default: "axiom-v0.3.0", choices: [ { name: "Axiom v0.3.0", value: "axiom-v0.3.0" }, { name: "PSE v0.3.0", value: "pse-v0.3.0" }, ], }); const packageName = await input({ message: "Halo2 Package Name:", default: circuitName .toLowerCase() .replace(/[^a-z0-9_]+/, "_") .replace(/_+/g, "_") .replace(/-+/g, "-"), validate: (input): boolean | string => { if (input.length === 0) { return "You must specify a package name."; } if (!/^[a-z0-9_]+(?:-[a-z0-9_]+)*$/.test(input)) { return ( "Package names must begin with a lowercase letter, number, or underscore, and only " + "be followed by lowercase or numeric characters and underscores (optionally " + "separated by hyphens)." ); } return true; }, }); // Collect `degree` as a positive integer. const degree: number = parseInt( await input({ message: "Degree:", default: "13", validate: (input): boolean | string => { if (input.length === 0) { return "You must specify a degree."; } if (!/^[1-9]\d*$/.test(input)) { return "Degree must be a positive integer."; } return true; }, }), 10, ); const threadBuilder: "GateThreadBuilder" | undefined = halo2Version === "axiom-v0.3.0" ? await select({ message: "Halo2 Base Version:", default: "GateThreadBuilder", choices: [ { name: "Gate Thread Builder", value: "GateThreadBuilder" }, ], }) : undefined; // Return the circuit path, depending on the halo2Version field const circuitPath: | "::circuit::EqualCircuit" | "::circuit_def::CircuitInput" = halo2Version !== "axiom-v0.3.0" ? "::circuit::EqualCircuit" : "::circuit_def::CircuitInput"; // Replace hyphens with underscores in the package name. const className = `${packageName.replace(/-/g, "_")}${circuitPath}`; templateDirectory = `${templateDirectory}/${halo2Version}`; Object.assign(context, { className, halo2Version, degree, packageName, threadBuilder, }); } else if (circuitType === "jolt") { // Jolt. const packageName = await input({ message: "Jolt Package Name:", default: circuitName .toLowerCase() .replace(/[^a-z0-9_]+/, "_") .replace(/_+/g, "_") .replace(/-+/g, "-"), validate: (input): boolean | string => { if (input.length === 0) { return "You must specify a package name."; } if (!/^[a-z0-9_]+(?:-[a-z0-9_]+)*$/.test(input)) { return ( "Package names must begin with a lowercase letter, number, or underscore, and only " + "be followed by lowercase or numeric characters and underscores (optionally " + "separated by hyphens)." ); } return true; }, }); const commitmentScheme: "hyperkzg" | "zeromorph" = await select({ message: "Commitment Scheme:", default: "hyperkzg", choices: [ { name: "HyperKZG", value: "hyperkzg" }, { name: "Zeromorph", value: "zeromorph" }, ], }); const stdEnabled: boolean = await confirm({ message: "Use Rust standard library:", default: true, }); Object.assign(context, { commitmentScheme, packageName, stdEnabled, }); } else if (circuitType === "noir") { const packageName = await input({ message: "Noir Package Name:", default: circuitName .toLowerCase() .replace(/[- ]/g, "_") .replace(/[^a-zA-Z0-9_]+/, "") .replace(/_+/g, "_"), validate: (input): boolean | string => { if (input.length === 0) { return "You must specify a package name."; } if (!/^[a-zA-Z0-9_]+$/.test(input)) { return "Package names must only contain alphanumeric characters and underscores."; } return true; }, }); const noirVersion: "0.17.0" | "0.18.0" | "0.19.4" | "0.22.0" | "0.23.0" = await select({ message: "Noir Version:", default: "0.23.0", choices: [ { name: "0.17.0", value: "0.17.0" }, { name: "0.18.0", value: "0.18.0" }, { name: "0.19.4", value: "0.19.4" }, { name: "0.22.0", value: "0.22.0" }, { name: "0.23.0", value: "0.23.0" }, ], }); const provingScheme: "barretenberg" = await select({ message: "Proving Scheme:", default: "barretenberg", choices: [{ name: "Barretenberg", value: "barretenberg" }], }); Object.assign(context, { packageName, noirVersion, provingScheme, }); } else if (circuitType === "plonky2") { const packageName = await input({ message: "Plonky2 Package Name:", default: circuitName .toLowerCase() .replace(/[^a-z0-9_]+/, "_") .replace(/_+/g, "_") .replace(/-+/g, "-"), validate: (input): boolean | string => { if (input.length === 0) { return "You must specify a package name."; } if (!/^[a-z0-9_]+(?:-[a-z0-9_]+)*$/.test(input)) { return ( "Package names must begin with a lowercase letter, number, or underscore, and only " + "be followed by lowercase or numeric characters and underscores (optionally " + "separated by hyphens)." ); } return true; }, }); const structName = await input({ message: "Full path to circuit struct:", default: "Circuit", validate: (input): boolean | string => { if (!/^[A-Z][A-Za-z0-9_]*$/.test(input)) { return ( "Struct name must begin with an uppercase letter and contain only " + "alphanumeric characters and underscores." ); } return true; }, }); const plonky2Version: "0.2.0" | "0.2.1" | "0.2.2" = await select({ message: "Plonky2 Version:", default: "0.2.2", choices: [ { name: "0.2.0", value: "0.2.0" }, { name: "0.2.1", value: "0.2.1" }, { name: "0.2.2", value: "0.2.2" }, ], }); Object.assign(context, { packageName, plonky2Version, structName, }); } else if (circuitType === "sp1") { const provingScheme: "core" | "compressed" | "groth16" | "plonk" = await select({ message: "Proving Scheme:", default: "groth16", choices: [ { name: "Compressed", value: "compressed" }, { name: "Core", value: "core" }, { name: "Groth16", value: "groth16" }, { name: "Plonk", value: "plonk" }, ], }); Object.assign(context, { provingScheme, }); } else { sindri.logger.fatal(`Sorry, ${circuitType} is not yet supported.`); return process.exit(1); } // Perform the scaffolding. sindri.logger.info( `Proceeding to generate scaffolded project in "${directoryPath}".`, ); await scaffoldDirectory("common", directoryPath, context, sindri.logger); await scaffoldDirectory( templateDirectory, directoryPath, context, sindri.logger, ); // We use this in `common` right now to keep the directory tracked, we can remove this once we // add files there. const gitKeepFile = path.join(directoryPath, ".gitkeep"); if (existsSync(gitKeepFile)) { rmSync(gitKeepFile); } sindri.logger.info("Project scaffolding successful."); // Install dependencies. if (circuitType === "circom") { let npmInstalled: boolean = false; try { execSync("npm --version"); npmInstalled = true; } catch { sindri.logger.warn( "NPM is not installed, cannot install circomlib as a dependency. " + "You will need to install NPM and run `npm install` yourself.", ); } if (npmInstalled) { sindri.logger.info("Installing circomlib."); execSync("npm install", { cwd: directoryPath }); } } // Optionally, initialize a git repository. let gitInstalled: boolean = false; try { execSync("git --version"); gitInstalled = true; } catch { sindri.logger.debug( "Git is not installed, skipping git initialization questions.", ); } const gitAlreadyInitialized = existsSync(path.join(directoryPath, ".git")); if (gitInstalled && !gitAlreadyInitialized) { const initializeGit = await confirm({ message: `Would you like to initialize a git repository in "${directoryPath}"?`, default: true, }); if (initializeGit) { sindri.logger.info( `Initializing git repository in "${directoryPath}".`, ); try { execSync("git init .", { cwd: directoryPath }); execSync("git add .", { cwd: directoryPath }); execSync("git commit -m 'Initial commit.'", { cwd: directoryPath }); sindri.logger.info("Successfully initialized git repository."); } catch (error) { sindri.logger.error( "Error occurred while initializing the git repository.", ); // Node.js doesn't seem to have a typed version of this error, so we assert it as // something that's at least in the right ballpark. const execError = error as NodeJS.ErrnoException & { output: Buffer | string; stderr: Buffer | string; stdout: Buffer | string; }; // The output is a really long list of numbers because it's a buffer, so truncate it. const noisyKeys: Array<"output" | "stderr" | "stdout"> = [ "output", "stderr", "stdout", ]; noisyKeys.forEach((key) => { if (key in execError) { execError[key] = "<truncated>"; } }); sindri.logger.error(execError); } } } });