From e18974b7a70839ee7d5d4f53d5e762ce455cc198 Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Thu, 29 May 2025 12:07:46 -0400 Subject: [PATCH 01/33] initial skeleton for build command --- bin/icp-cli/src/commands.rs | 8 +++++++- bin/icp-cli/src/commands/build.rs | 13 +++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 bin/icp-cli/src/commands/build.rs diff --git a/bin/icp-cli/src/commands.rs b/bin/icp-cli/src/commands.rs index cdfe90e0f..5f9e2040c 100644 --- a/bin/icp-cli/src/commands.rs +++ b/bin/icp-cli/src/commands.rs @@ -1,7 +1,8 @@ -use crate::commands::network::NetworkCommandError; +use crate::commands::{build::BuildCommandError, network::NetworkCommandError}; use clap::{Parser, Subcommand}; use snafu::Snafu; +mod build; mod network; #[derive(Parser, Debug)] @@ -12,11 +13,13 @@ pub struct Cli { #[derive(Subcommand, Debug)] pub enum Subcmd { + Build(build::Cmd), Network(network::Cmd), } pub async fn dispatch(cli: Cli) -> Result<(), DispatchError> { match cli.subcommand { + Subcmd::Build(opts) => build::dispatch(opts).await?, Subcmd::Network(opts) => network::dispatch(opts).await?, } Ok(()) @@ -24,6 +27,9 @@ pub async fn dispatch(cli: Cli) -> Result<(), DispatchError> { #[derive(Debug, Snafu)] pub enum DispatchError { + #[snafu(transparent)] + Build { source: BuildCommandError }, + #[snafu(transparent)] Network { source: NetworkCommandError }, } diff --git a/bin/icp-cli/src/commands/build.rs b/bin/icp-cli/src/commands/build.rs new file mode 100644 index 000000000..9f57128cf --- /dev/null +++ b/bin/icp-cli/src/commands/build.rs @@ -0,0 +1,13 @@ +use clap::Parser; +use snafu::Snafu; + +#[derive(Parser, Debug)] +pub struct Cmd; + +pub async fn dispatch(_cmd: Cmd) -> Result<(), BuildCommandError> { + Ok(()) +} + +#[derive(Debug, Snafu)] +#[snafu(display("Failed to build canister"))] +pub struct BuildCommandError {} From 72452ee90240100f9fef0c4145cef4b4ba5456c7 Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Thu, 29 May 2025 12:33:56 -0400 Subject: [PATCH 02/33] pull project functionality into its own crate --- Cargo.lock | 22 +++++++++++++++++++ Cargo.toml | 4 +++- bin/icp-cli/Cargo.toml | 4 +++- bin/icp-cli/src/commands/network/run.rs | 2 +- bin/icp-cli/src/main.rs | 1 - bin/icp-cli/src/project.rs | 1 - lib/icp-canister/Cargo.toml | 9 ++++++++ lib/icp-canister/src/lib.rs | 3 +++ lib/icp-project/Cargo.toml | 11 ++++++++++ .../icp-project/src/lib.rs | 0 10 files changed, 52 insertions(+), 5 deletions(-) delete mode 100644 bin/icp-cli/src/project.rs create mode 100644 lib/icp-canister/Cargo.toml create mode 100644 lib/icp-canister/src/lib.rs create mode 100644 lib/icp-project/Cargo.toml rename bin/icp-cli/src/project/structure.rs => lib/icp-project/src/lib.rs (100%) diff --git a/Cargo.lock b/Cargo.lock index bf44fe563..283ddd05b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1195,14 +1195,25 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "icp-canister" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "snafu", +] + [[package]] name = "icp-cli" version = "0.1.0" dependencies = [ "assert_cmd", "clap", + "icp-canister", "icp-fs", "icp-network", + "icp-project", "predicates", "reqwest", "snafu", @@ -1242,6 +1253,17 @@ dependencies = [ "uuid", ] +[[package]] +name = "icp-project" +version = "0.1.0" +dependencies = [ + "icp-canister", + "icp-network", + "serde", + "serde_json", + "snafu", +] + [[package]] name = "icu_collections" version = "2.0.0" diff --git a/Cargo.toml b/Cargo.toml index 369e08c03..9f3257760 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,11 @@ [workspace] members = [ "bin/icp-cli", + "lib/icp-canister", + "lib/icp-fs", "lib/icp-identity", "lib/icp-network", - "lib/icp-fs" + "lib/icp-project", ] resolver = "3" diff --git a/bin/icp-cli/Cargo.toml b/bin/icp-cli/Cargo.toml index be0b294dc..e14b4ee0d 100644 --- a/bin/icp-cli/Cargo.toml +++ b/bin/icp-cli/Cargo.toml @@ -10,8 +10,10 @@ path = "src/main.rs" [dependencies] clap = { version = "4.5.35", features = ["derive"] } -icp-network = { path = "../../lib/icp-network" } +icp-canister = { path = "../../lib/icp-canister" } icp-fs = { path = "../../lib/icp-fs" } +icp-network = { path = "../../lib/icp-network" } +icp-project = { path = "../../lib/icp-project" } snafu = { workspace = true } tokio = { workspace = true } diff --git a/bin/icp-cli/src/commands/network/run.rs b/bin/icp-cli/src/commands/network/run.rs index 45c1c200c..9cdab5598 100644 --- a/bin/icp-cli/src/commands/network/run.rs +++ b/bin/icp-cli/src/commands/network/run.rs @@ -1,7 +1,7 @@ use crate::commands::network::run::RunNetworkCommandError::ProjectStructureNotFound; -use crate::project::structure::ProjectDirectoryStructure; use clap::Parser; use icp_network::{ManagedNetworkModel, RunNetworkError, run_network}; +use icp_project::ProjectDirectoryStructure; use snafu::Snafu; /// Run the local network diff --git a/bin/icp-cli/src/main.rs b/bin/icp-cli/src/main.rs index 579157bb5..e87744e2b 100644 --- a/bin/icp-cli/src/main.rs +++ b/bin/icp-cli/src/main.rs @@ -2,7 +2,6 @@ use crate::commands::Cli; use clap::Parser; mod commands; -mod project; #[tokio::main] async fn main() { diff --git a/bin/icp-cli/src/project.rs b/bin/icp-cli/src/project.rs deleted file mode 100644 index 26a4f5f42..000000000 --- a/bin/icp-cli/src/project.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod structure; diff --git a/lib/icp-canister/Cargo.toml b/lib/icp-canister/Cargo.toml new file mode 100644 index 000000000..fea286d42 --- /dev/null +++ b/lib/icp-canister/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "icp-canister" +version = "0.1.0" +edition = "2024" + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +snafu = { workspace = true } diff --git a/lib/icp-canister/src/lib.rs b/lib/icp-canister/src/lib.rs new file mode 100644 index 000000000..f35a5ee02 --- /dev/null +++ b/lib/icp-canister/src/lib.rs @@ -0,0 +1,3 @@ +struct Manifest { + name: String, +} diff --git a/lib/icp-project/Cargo.toml b/lib/icp-project/Cargo.toml new file mode 100644 index 000000000..bdcfe4c29 --- /dev/null +++ b/lib/icp-project/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "icp-project" +version = "0.1.0" +edition = "2024" + +[dependencies] +icp-canister = { path = "../icp-canister" } +icp-network = { path = "../icp-network" } +serde = { workspace = true } +serde_json = { workspace = true } +snafu = { workspace = true } diff --git a/bin/icp-cli/src/project/structure.rs b/lib/icp-project/src/lib.rs similarity index 100% rename from bin/icp-cli/src/project/structure.rs rename to lib/icp-project/src/lib.rs From d76e032dc533593452be36afb16e26bdba65f09e Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Thu, 29 May 2025 14:11:23 -0400 Subject: [PATCH 03/33] create canister and project manifest definitions --- lib/icp-canister/src/lib.rs | 48 +++++++++++++++++++++++++++++++++++-- lib/icp-project/src/lib.rs | 14 +++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/lib/icp-canister/src/lib.rs b/lib/icp-canister/src/lib.rs index f35a5ee02..6c6937555 100644 --- a/lib/icp-canister/src/lib.rs +++ b/lib/icp-canister/src/lib.rs @@ -1,3 +1,47 @@ -struct Manifest { - name: String, +use serde::Deserialize; + +/// Represents the manifest describing a single canister, +/// including its name and how it should be built. +#[derive(Debug, Deserialize)] +pub struct CanisterManifest { + /// Name of the canister described by this manifest. + pub name: String, + + /// Build configuration for producing the canister's WebAssembly. + pub build: Build, +} + +/// Describes how the canister should be built into WebAssembly, +/// including the adapter responsible for the build. +#[derive(Debug, Deserialize)] +pub struct Build { + pub adapter: Adapter, +} + +/// Identifies the type of adapter used to build the canister, +/// e.g. "motoko", "rust", or "custom". +#[derive(Debug, Deserialize)] +pub struct Adapter { + #[serde(rename = "type")] + pub typ: AdapterType, +} + +/// Known adapter types that can be used to build a canister. +/// These correspond to the values found in `build.adapter.type` in the YAML. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum AdapterType { + /// A canister written in Rust. + Rust, + + /// A canister written in Motoko. + Motoko, + + /// A canister built using custom instructions, + /// such as a shell script or other manual build process. + Custom, + + /// An assets canister used to serve front-end applications + /// or static assets on the Internet Computer. + Assets, } diff --git a/lib/icp-project/src/lib.rs b/lib/icp-project/src/lib.rs index a8b03f56f..9e22cc1bc 100644 --- a/lib/icp-project/src/lib.rs +++ b/lib/icp-project/src/lib.rs @@ -1,4 +1,5 @@ use icp_network::structure::NetworkDirectoryStructure; +use serde::Deserialize; use std::path::PathBuf; pub struct ProjectDirectoryStructure { @@ -38,3 +39,16 @@ impl ProjectDirectoryStructure { NetworkDirectoryStructure::new(&network_root) } } + +/// Represents the manifest for an ICP project, typically loaded from `icp.yaml`. +/// A project is a repository or directory grouping related canisters and network definitions. +#[derive(Debug, Deserialize)] +pub struct ProjectManifest { + /// List of canister manifests belonging to this project. + /// Supports glob patterns to specify multiple canister YAML files. + pub canisters: Vec, + + /// List of network definition files relevant to the project. + /// Supports glob patterns to reference multiple network config files. + pub networks: Vec, +} From 45507b8f1af79f1cb6a7f5592f7342ec219f0421 Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Thu, 29 May 2025 16:33:21 -0400 Subject: [PATCH 04/33] add parsing logic for project and canister manifests --- Cargo.lock | 28 ++++++++++++ Cargo.toml | 2 + bin/icp-cli/src/commands/build.rs | 61 ++++++++++++++++++++++++-- lib/icp-canister/Cargo.toml | 1 + lib/icp-canister/src/lib.rs | 70 ++++++++++++++++++----------- lib/icp-project/Cargo.toml | 2 + lib/icp-project/src/lib.rs | 73 +++++++++++++++++++++++++------ 7 files changed, 195 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 283ddd05b..dadd55bf2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -872,6 +872,12 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + [[package]] name = "group" version = "0.13.0" @@ -1201,6 +1207,7 @@ version = "0.1.0" dependencies = [ "serde", "serde_json", + "serde_yaml", "snafu", ] @@ -1257,10 +1264,12 @@ dependencies = [ name = "icp-project" version = "0.1.0" dependencies = [ + "glob", "icp-canister", "icp-network", "serde", "serde_json", + "serde_yaml", "snafu", ] @@ -2363,6 +2372,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "sha2" version = "0.9.9" @@ -2950,6 +2972,12 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index 9f3257760..4ff808fc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ resolver = "3" [workspace.dependencies] candid = "0.10.14" fd-lock = "4.0.4" +glob = "0.3.2" hex = "0.4.3" ic-agent = { version = "0.40.1" } pocket-ic = "9.0.0" @@ -21,6 +22,7 @@ reqwest = { version = "0.12.15", default-features = false, features = [ ] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +serde_yaml = "0.9.34" snafu = "0.8.5" tempfile = "3" time = "0.3.9" diff --git a/bin/icp-cli/src/commands/build.rs b/bin/icp-cli/src/commands/build.rs index 9f57128cf..6e1666b6a 100644 --- a/bin/icp-cli/src/commands/build.rs +++ b/bin/icp-cli/src/commands/build.rs @@ -1,13 +1,68 @@ +use std::path::PathBuf; + use clap::Parser; -use snafu::Snafu; +use icp_canister::{CanisterManifest, CanisterManifestError}; +use snafu::{ResultExt, Snafu}; + +use icp_fs::fs::{ReadFileError, read}; +use icp_project::{ProjectDirectoryStructure, ProjectManifest, ProjectManifestError}; #[derive(Parser, Debug)] pub struct Cmd; pub async fn dispatch(_cmd: Cmd) -> Result<(), BuildCommandError> { + // Open project + let pds = ProjectDirectoryStructure::find().ok_or(BuildCommandError::ProjectNotFound)?; + + let mpath = pds.root().join("icp.yaml"); + if !mpath.exists() { + return Err(BuildCommandError::ProjectNotFound); + } + + let bs = read(mpath).context(ProjectLoadSnafu)?; + let pm = ProjectManifest::from_bytes(&bs).context(ProjectParseSnafu)?; + + println!("{pm:?}"); + + // List canisters in project + let mut cs = Vec::new(); + + for c in pm.canisters { + let mpath = c.join("canister.yaml"); + if !mpath.exists() { + return Err(BuildCommandError::CanisterNotFound { + path: format!("{mpath:?}"), + }); + } + + let bs = read(mpath).context(CanisterLoadSnafu)?; + let cm = CanisterManifest::from_bytes(&bs).context(CanisterParseSnafu)?; + + cs.push(cm); + } + + // Build canisters + Ok(()) } #[derive(Debug, Snafu)] -#[snafu(display("Failed to build canister"))] -pub struct BuildCommandError {} +pub enum BuildCommandError { + #[snafu(display("no project (icp.yaml) found in current directory or its parents"))] + ProjectNotFound, + + #[snafu(display("failed to load project manifest: {source}"))] + ProjectLoad { source: ReadFileError }, + + #[snafu(display("failed to parse project manifest: {source}"))] + ProjectParse { source: ProjectManifestError }, + + #[snafu(display("canister manifest not found: {path}"))] + CanisterNotFound { path: String }, + + #[snafu(display("failed to load canister manifest: {source}"))] + CanisterLoad { source: ReadFileError }, + + #[snafu(display("failed to parse canister manifest: {source}"))] + CanisterParse { source: CanisterManifestError }, +} diff --git a/lib/icp-canister/Cargo.toml b/lib/icp-canister/Cargo.toml index fea286d42..56aa31cb9 100644 --- a/lib/icp-canister/Cargo.toml +++ b/lib/icp-canister/Cargo.toml @@ -6,4 +6,5 @@ edition = "2024" [dependencies] serde = { workspace = true } serde_json = { workspace = true } +serde_yaml = { workspace = true } snafu = { workspace = true } diff --git a/lib/icp-canister/src/lib.rs b/lib/icp-canister/src/lib.rs index 6c6937555..75eb24ab9 100644 --- a/lib/icp-canister/src/lib.rs +++ b/lib/icp-canister/src/lib.rs @@ -1,30 +1,5 @@ use serde::Deserialize; - -/// Represents the manifest describing a single canister, -/// including its name and how it should be built. -#[derive(Debug, Deserialize)] -pub struct CanisterManifest { - /// Name of the canister described by this manifest. - pub name: String, - - /// Build configuration for producing the canister's WebAssembly. - pub build: Build, -} - -/// Describes how the canister should be built into WebAssembly, -/// including the adapter responsible for the build. -#[derive(Debug, Deserialize)] -pub struct Build { - pub adapter: Adapter, -} - -/// Identifies the type of adapter used to build the canister, -/// e.g. "motoko", "rust", or "custom". -#[derive(Debug, Deserialize)] -pub struct Adapter { - #[serde(rename = "type")] - pub typ: AdapterType, -} +use snafu::{ResultExt, Snafu}; /// Known adapter types that can be used to build a canister. /// These correspond to the values found in `build.adapter.type` in the YAML. @@ -45,3 +20,46 @@ pub enum AdapterType { /// or static assets on the Internet Computer. Assets, } + +/// Identifies the type of adapter used to build the canister, +/// e.g. "motoko", "rust", or "custom". +#[derive(Debug, Deserialize)] +pub struct Adapter { + #[serde(rename = "type")] + pub typ: AdapterType, +} + +/// Describes how the canister should be built into WebAssembly, +/// including the adapter responsible for the build. +#[derive(Debug, Deserialize)] +pub struct Build { + pub adapter: Adapter, +} + +/// Represents the manifest describing a single canister, +/// including its name and how it should be built. +#[derive(Debug, Deserialize)] +pub struct CanisterManifest { + /// Name of the canister described by this manifest. + pub name: String, + + /// Build configuration for producing the canister's WebAssembly. + pub build: Build, +} + +impl CanisterManifest { + pub fn from_bytes>(bytes: B) -> Result { + let cm: CanisterManifest = serde_yaml::from_slice(bytes.as_ref()).context(ParseSnafu)?; + + Ok(cm) + } +} + +#[derive(Debug, Snafu)] +pub enum CanisterManifestError { + #[snafu(display("failed to parse canister manifest: {}", source))] + Parse { source: serde_yaml::Error }, + + #[snafu(display("invalid UTF-8 in canister path"))] + InvalidPathUtf8, +} diff --git a/lib/icp-project/Cargo.toml b/lib/icp-project/Cargo.toml index bdcfe4c29..4715bf547 100644 --- a/lib/icp-project/Cargo.toml +++ b/lib/icp-project/Cargo.toml @@ -4,8 +4,10 @@ version = "0.1.0" edition = "2024" [dependencies] +glob = { workspace = true } icp-canister = { path = "../icp-canister" } icp-network = { path = "../icp-network" } serde = { workspace = true } serde_json = { workspace = true } +serde_yaml = { workspace = true } snafu = { workspace = true } diff --git a/lib/icp-project/src/lib.rs b/lib/icp-project/src/lib.rs index 9e22cc1bc..d9e31870f 100644 --- a/lib/icp-project/src/lib.rs +++ b/lib/icp-project/src/lib.rs @@ -1,7 +1,67 @@ use icp_network::structure::NetworkDirectoryStructure; use serde::Deserialize; +use snafu::{OptionExt, ResultExt, Snafu}; use std::path::PathBuf; +/// Represents the manifest for an ICP project, typically loaded from `icp.yaml`. +/// A project is a repository or directory grouping related canisters and network definitions. +#[derive(Debug, Deserialize)] +pub struct ProjectManifest { + /// List of canister manifests belonging to this project. + /// Supports glob patterns to specify multiple canister YAML files. + #[serde(default)] + pub canisters: Vec, + + /// List of network definition files relevant to the project. + /// Supports glob patterns to reference multiple network config files. + #[serde(default)] + pub networks: Vec, +} + +impl ProjectManifest { + pub fn from_bytes>(bytes: B) -> Result { + let mut pm: ProjectManifest = serde_yaml::from_slice(bytes.as_ref()).context(ParseSnafu)?; + + // Project canisters + let mut cs = Vec::new(); + + for pattern in pm.canisters { + let pattern = pattern.to_str().context(InvalidPathUtf8Snafu)?; + let matches = glob::glob(pattern).context(GlobPatternSnafu)?; + + for c in matches { + let path = c.context(GlobWalkSnafu)?; + + // Skip non-canister directories + if !path.join("canister.yaml").exists() { + continue; + } + + cs.push(path); + } + } + + pm.canisters = cs; + + Ok(pm) + } +} + +#[derive(Debug, Snafu)] +pub enum ProjectManifestError { + #[snafu(display("failed to parse project manifest: {}", source))] + Parse { source: serde_yaml::Error }, + + #[snafu(display("invalid UTF-8 in canister path"))] + InvalidPathUtf8, + + #[snafu(display("invalid glob pattern in manifest: {}", source))] + GlobPattern { source: glob::PatternError }, + + #[snafu(display("failed while reading glob matches: {}", source))] + GlobWalk { source: glob::GlobError }, +} + pub struct ProjectDirectoryStructure { root: PathBuf, } @@ -39,16 +99,3 @@ impl ProjectDirectoryStructure { NetworkDirectoryStructure::new(&network_root) } } - -/// Represents the manifest for an ICP project, typically loaded from `icp.yaml`. -/// A project is a repository or directory grouping related canisters and network definitions. -#[derive(Debug, Deserialize)] -pub struct ProjectManifest { - /// List of canister manifests belonging to this project. - /// Supports glob patterns to specify multiple canister YAML files. - pub canisters: Vec, - - /// List of network definition files relevant to the project. - /// Supports glob patterns to reference multiple network config files. - pub networks: Vec, -} From a40140e14cac4875400405e6a95937bedd4f437d Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Thu, 29 May 2025 16:42:34 -0400 Subject: [PATCH 05/33] revert some minor changes --- bin/icp-cli/src/commands/build.rs | 8 ++--- bin/icp-cli/src/commands/network/run.rs | 2 +- bin/icp-cli/src/main.rs | 1 + bin/icp-cli/src/project.rs | 1 + bin/icp-cli/src/project/structure.rs | 40 +++++++++++++++++++++++++ lib/icp-project/src/lib.rs | 39 ------------------------ 6 files changed, 46 insertions(+), 45 deletions(-) create mode 100644 bin/icp-cli/src/project.rs create mode 100644 bin/icp-cli/src/project/structure.rs diff --git a/bin/icp-cli/src/commands/build.rs b/bin/icp-cli/src/commands/build.rs index 6e1666b6a..c4c7fbddc 100644 --- a/bin/icp-cli/src/commands/build.rs +++ b/bin/icp-cli/src/commands/build.rs @@ -1,11 +1,11 @@ -use std::path::PathBuf; - use clap::Parser; use icp_canister::{CanisterManifest, CanisterManifestError}; use snafu::{ResultExt, Snafu}; use icp_fs::fs::{ReadFileError, read}; -use icp_project::{ProjectDirectoryStructure, ProjectManifest, ProjectManifestError}; +use icp_project::{ProjectManifest, ProjectManifestError}; + +use crate::project::structure::ProjectDirectoryStructure; #[derive(Parser, Debug)] pub struct Cmd; @@ -22,8 +22,6 @@ pub async fn dispatch(_cmd: Cmd) -> Result<(), BuildCommandError> { let bs = read(mpath).context(ProjectLoadSnafu)?; let pm = ProjectManifest::from_bytes(&bs).context(ProjectParseSnafu)?; - println!("{pm:?}"); - // List canisters in project let mut cs = Vec::new(); diff --git a/bin/icp-cli/src/commands/network/run.rs b/bin/icp-cli/src/commands/network/run.rs index 9cdab5598..45c1c200c 100644 --- a/bin/icp-cli/src/commands/network/run.rs +++ b/bin/icp-cli/src/commands/network/run.rs @@ -1,7 +1,7 @@ use crate::commands::network::run::RunNetworkCommandError::ProjectStructureNotFound; +use crate::project::structure::ProjectDirectoryStructure; use clap::Parser; use icp_network::{ManagedNetworkModel, RunNetworkError, run_network}; -use icp_project::ProjectDirectoryStructure; use snafu::Snafu; /// Run the local network diff --git a/bin/icp-cli/src/main.rs b/bin/icp-cli/src/main.rs index e87744e2b..579157bb5 100644 --- a/bin/icp-cli/src/main.rs +++ b/bin/icp-cli/src/main.rs @@ -2,6 +2,7 @@ use crate::commands::Cli; use clap::Parser; mod commands; +mod project; #[tokio::main] async fn main() { diff --git a/bin/icp-cli/src/project.rs b/bin/icp-cli/src/project.rs new file mode 100644 index 000000000..26a4f5f42 --- /dev/null +++ b/bin/icp-cli/src/project.rs @@ -0,0 +1 @@ +pub mod structure; diff --git a/bin/icp-cli/src/project/structure.rs b/bin/icp-cli/src/project/structure.rs new file mode 100644 index 000000000..a8b03f56f --- /dev/null +++ b/bin/icp-cli/src/project/structure.rs @@ -0,0 +1,40 @@ +use icp_network::structure::NetworkDirectoryStructure; +use std::path::PathBuf; + +pub struct ProjectDirectoryStructure { + root: PathBuf, +} + +impl ProjectDirectoryStructure { + pub fn find() -> Option { + let current_dir = std::env::current_dir().ok()?; + let mut path = current_dir.clone(); + loop { + if path.join("icp.yaml").exists() { + break Some(Self { root: path }); + } + if !path.pop() { + break None; + } + } + } + + pub fn root(&self) -> &PathBuf { + &self.root + } + + #[allow(dead_code)] + pub fn network_config_path(&self, name: &str) -> PathBuf { + self.root.join("networks").join(format!("{name}.yaml")) + } + + fn work_dir(&self) -> PathBuf { + self.root.join(".icp") + } + + pub fn network(&self, network_name: &str) -> NetworkDirectoryStructure { + let network_root = self.work_dir().join("networks").join(network_name); + + NetworkDirectoryStructure::new(&network_root) + } +} diff --git a/lib/icp-project/src/lib.rs b/lib/icp-project/src/lib.rs index d9e31870f..74797219e 100644 --- a/lib/icp-project/src/lib.rs +++ b/lib/icp-project/src/lib.rs @@ -1,4 +1,3 @@ -use icp_network::structure::NetworkDirectoryStructure; use serde::Deserialize; use snafu::{OptionExt, ResultExt, Snafu}; use std::path::PathBuf; @@ -61,41 +60,3 @@ pub enum ProjectManifestError { #[snafu(display("failed while reading glob matches: {}", source))] GlobWalk { source: glob::GlobError }, } - -pub struct ProjectDirectoryStructure { - root: PathBuf, -} - -impl ProjectDirectoryStructure { - pub fn find() -> Option { - let current_dir = std::env::current_dir().ok()?; - let mut path = current_dir.clone(); - loop { - if path.join("icp.yaml").exists() { - break Some(Self { root: path }); - } - if !path.pop() { - break None; - } - } - } - - pub fn root(&self) -> &PathBuf { - &self.root - } - - #[allow(dead_code)] - pub fn network_config_path(&self, name: &str) -> PathBuf { - self.root.join("networks").join(format!("{name}.yaml")) - } - - fn work_dir(&self) -> PathBuf { - self.root.join(".icp") - } - - pub fn network(&self, network_name: &str) -> NetworkDirectoryStructure { - let network_root = self.work_dir().join("networks").join(network_name); - - NetworkDirectoryStructure::new(&network_root) - } -} From c6c3de82879f8cb6e1e1d46b905c1981f5427e9e Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Thu, 29 May 2025 17:11:46 -0400 Subject: [PATCH 06/33] put adapter definitions in adapter enum --- bin/icp-cli/src/commands/build.rs | 22 +++++----- lib/icp-canister/src/lib.rs | 67 +++++++++++++++++++++---------- 2 files changed, 56 insertions(+), 33 deletions(-) diff --git a/bin/icp-cli/src/commands/build.rs b/bin/icp-cli/src/commands/build.rs index c4c7fbddc..dd792418a 100644 --- a/bin/icp-cli/src/commands/build.rs +++ b/bin/icp-cli/src/commands/build.rs @@ -19,8 +19,8 @@ pub async fn dispatch(_cmd: Cmd) -> Result<(), BuildCommandError> { return Err(BuildCommandError::ProjectNotFound); } - let bs = read(mpath).context(ProjectLoadSnafu)?; - let pm = ProjectManifest::from_bytes(&bs).context(ProjectParseSnafu)?; + let bs = read(mpath)?; + let pm = ProjectManifest::from_bytes(&bs)?; // List canisters in project let mut cs = Vec::new(); @@ -33,13 +33,14 @@ pub async fn dispatch(_cmd: Cmd) -> Result<(), BuildCommandError> { }); } - let bs = read(mpath).context(CanisterLoadSnafu)?; - let cm = CanisterManifest::from_bytes(&bs).context(CanisterParseSnafu)?; + let bs = read(mpath)?; + let cm = CanisterManifest::from_bytes(&bs)?; cs.push(cm); } // Build canisters + println!("{cs:?}"); Ok(()) } @@ -49,18 +50,15 @@ pub enum BuildCommandError { #[snafu(display("no project (icp.yaml) found in current directory or its parents"))] ProjectNotFound, - #[snafu(display("failed to load project manifest: {source}"))] - ProjectLoad { source: ReadFileError }, - - #[snafu(display("failed to parse project manifest: {source}"))] + #[snafu(transparent)] ProjectParse { source: ProjectManifestError }, #[snafu(display("canister manifest not found: {path}"))] CanisterNotFound { path: String }, - #[snafu(display("failed to load canister manifest: {source}"))] - CanisterLoad { source: ReadFileError }, - - #[snafu(display("failed to parse canister manifest: {source}"))] + #[snafu(transparent)] CanisterParse { source: CanisterManifestError }, + + #[snafu(transparent)] + ReadFile { source: ReadFileError }, } diff --git a/lib/icp-canister/src/lib.rs b/lib/icp-canister/src/lib.rs index 75eb24ab9..0444ea94e 100644 --- a/lib/icp-canister/src/lib.rs +++ b/lib/icp-canister/src/lib.rs @@ -1,32 +1,60 @@ use serde::Deserialize; use snafu::{ResultExt, Snafu}; -/// Known adapter types that can be used to build a canister. -/// These correspond to the values found in `build.adapter.type` in the YAML. +/// Configuration for a Rust-based canister build adapter. #[derive(Debug, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum AdapterType { - /// A canister written in Rust. - Rust, +pub struct RustAdapter { + /// The name of the Cargo package to build. + pub package: String, +} - /// A canister written in Motoko. - Motoko, +/// Configuration for a Motoko-based canister build adapter. +#[derive(Debug, Deserialize)] +pub struct MotokoAdapter { + /// Optional path to the main Motoko source file. + /// If omitted, a default like `main.mo` may be assumed. + #[serde(default)] + pub main: Option, +} - /// A canister built using custom instructions, - /// such as a shell script or other manual build process. - Custom, +/// Configuration for a custom canister build adapter. +#[derive(Debug, Deserialize)] +pub struct CustomAdapter { + /// Path to a script or executable used to build the canister. + pub script: String, +} - /// An assets canister used to serve front-end applications - /// or static assets on the Internet Computer. - Assets, +/// Configuration for an asset canister used to serve static content. +#[derive(Debug, Deserialize)] +pub struct AssetsAdapter { + /// Directory containing the static assets to include in the canister. + pub source: String, } /// Identifies the type of adapter used to build the canister, -/// e.g. "motoko", "rust", or "custom". +/// along with its configuration. +/// +/// The adapter type is specified via the `type` field in the YAML file. +/// For example: +/// +/// ```yaml +/// type: rust +/// package: my_canister +/// ``` #[derive(Debug, Deserialize)] -pub struct Adapter { - #[serde(rename = "type")] - pub typ: AdapterType, +#[serde(tag = "type", rename_all = "lowercase")] +pub enum Adapter { + /// A canister written in Rust. + Rust(RustAdapter), + + /// A canister written in Motoko. + Motoko(MotokoAdapter), + + /// A canister built using a custom script or command. + Custom(CustomAdapter), + + /// A static asset canister. + Assets(AssetsAdapter), } /// Describes how the canister should be built into WebAssembly, @@ -59,7 +87,4 @@ impl CanisterManifest { pub enum CanisterManifestError { #[snafu(display("failed to parse canister manifest: {}", source))] Parse { source: serde_yaml::Error }, - - #[snafu(display("invalid UTF-8 in canister path"))] - InvalidPathUtf8, } From 14aeab576207bb911167ddbbe21a7f492b98fd7e Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Thu, 29 May 2025 17:14:36 -0400 Subject: [PATCH 07/33] clean up snafu errors --- bin/icp-cli/src/commands/build.rs | 2 +- lib/icp-project/src/lib.rs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bin/icp-cli/src/commands/build.rs b/bin/icp-cli/src/commands/build.rs index dd792418a..97bd78b7d 100644 --- a/bin/icp-cli/src/commands/build.rs +++ b/bin/icp-cli/src/commands/build.rs @@ -1,6 +1,6 @@ use clap::Parser; use icp_canister::{CanisterManifest, CanisterManifestError}; -use snafu::{ResultExt, Snafu}; +use snafu::Snafu; use icp_fs::fs::{ReadFileError, read}; use icp_project::{ProjectManifest, ProjectManifestError}; diff --git a/lib/icp-project/src/lib.rs b/lib/icp-project/src/lib.rs index 74797219e..2e48b1804 100644 --- a/lib/icp-project/src/lib.rs +++ b/lib/icp-project/src/lib.rs @@ -26,10 +26,10 @@ impl ProjectManifest { for pattern in pm.canisters { let pattern = pattern.to_str().context(InvalidPathUtf8Snafu)?; - let matches = glob::glob(pattern).context(GlobPatternSnafu)?; + let matches = glob::glob(pattern)?; - for c in matches { - let path = c.context(GlobWalkSnafu)?; + for path in matches { + let path = path?; // Skip non-canister directories if !path.join("canister.yaml").exists() { @@ -54,9 +54,9 @@ pub enum ProjectManifestError { #[snafu(display("invalid UTF-8 in canister path"))] InvalidPathUtf8, - #[snafu(display("invalid glob pattern in manifest: {}", source))] + #[snafu(transparent)] GlobPattern { source: glob::PatternError }, - #[snafu(display("failed while reading glob matches: {}", source))] + #[snafu(transparent)] GlobWalk { source: glob::GlobError }, } From fd56d1c1904ccb3f5151cba0f2a56c1a896e37eb Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Thu, 29 May 2025 17:34:14 -0400 Subject: [PATCH 08/33] switch from parsing yaml bytes to taking the path so we can report better errors --- Cargo.lock | 2 ++ bin/icp-cli/src/commands/build.rs | 51 +++++++++++++------------------ lib/icp-canister/Cargo.toml | 1 + lib/icp-canister/src/lib.rs | 27 +++++++++++++--- lib/icp-project/Cargo.toml | 1 + lib/icp-project/src/lib.rs | 28 ++++++++++++++--- 6 files changed, 71 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dadd55bf2..01396a651 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1205,6 +1205,7 @@ dependencies = [ name = "icp-canister" version = "0.1.0" dependencies = [ + "icp-fs", "serde", "serde_json", "serde_yaml", @@ -1266,6 +1267,7 @@ version = "0.1.0" dependencies = [ "glob", "icp-canister", + "icp-fs", "icp-network", "serde", "serde_json", diff --git a/bin/icp-cli/src/commands/build.rs b/bin/icp-cli/src/commands/build.rs index 97bd78b7d..366b47ced 100644 --- a/bin/icp-cli/src/commands/build.rs +++ b/bin/icp-cli/src/commands/build.rs @@ -1,8 +1,9 @@ +use std::path::PathBuf; + use clap::Parser; -use icp_canister::{CanisterManifest, CanisterManifestError}; -use snafu::Snafu; +use snafu::{ResultExt, Snafu}; -use icp_fs::fs::{ReadFileError, read}; +use icp_canister::{CanisterManifest, CanisterManifestError}; use icp_project::{ProjectManifest, ProjectManifestError}; use crate::project::structure::ProjectDirectoryStructure; @@ -11,30 +12,20 @@ use crate::project::structure::ProjectDirectoryStructure; pub struct Cmd; pub async fn dispatch(_cmd: Cmd) -> Result<(), BuildCommandError> { - // Open project - let pds = ProjectDirectoryStructure::find().ok_or(BuildCommandError::ProjectNotFound)?; - - let mpath = pds.root().join("icp.yaml"); - if !mpath.exists() { - return Err(BuildCommandError::ProjectNotFound); - } + let mpath = ProjectDirectoryStructure::find() + .ok_or(BuildCommandError::ProjectNotFound)? + .root() + .join("icp.yaml"); - let bs = read(mpath)?; - let pm = ProjectManifest::from_bytes(&bs)?; + let pm = ProjectManifest::from_file(&mpath).context(ProjectLoadSnafu { path: mpath })?; // List canisters in project let mut cs = Vec::new(); for c in pm.canisters { let mpath = c.join("canister.yaml"); - if !mpath.exists() { - return Err(BuildCommandError::CanisterNotFound { - path: format!("{mpath:?}"), - }); - } - let bs = read(mpath)?; - let cm = CanisterManifest::from_bytes(&bs)?; + let cm = CanisterManifest::from_file(&mpath).context(CanisterLoadSnafu { path: mpath })?; cs.push(cm); } @@ -50,15 +41,15 @@ pub enum BuildCommandError { #[snafu(display("no project (icp.yaml) found in current directory or its parents"))] ProjectNotFound, - #[snafu(transparent)] - ProjectParse { source: ProjectManifestError }, - - #[snafu(display("canister manifest not found: {path}"))] - CanisterNotFound { path: String }, - - #[snafu(transparent)] - CanisterParse { source: CanisterManifestError }, - - #[snafu(transparent)] - ReadFile { source: ReadFileError }, + #[snafu(display("failed to load project manifest: {path:?}"))] + ProjectLoad { + source: ProjectManifestError, + path: PathBuf, + }, + + #[snafu(display("failed to load canister manifest: {path:?}"))] + CanisterLoad { + source: CanisterManifestError, + path: PathBuf, + }, } diff --git a/lib/icp-canister/Cargo.toml b/lib/icp-canister/Cargo.toml index 56aa31cb9..28fac5cc8 100644 --- a/lib/icp-canister/Cargo.toml +++ b/lib/icp-canister/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +icp-fs = { "path" = "../icp-fs" } serde = { workspace = true } serde_json = { workspace = true } serde_yaml = { workspace = true } diff --git a/lib/icp-canister/src/lib.rs b/lib/icp-canister/src/lib.rs index 0444ea94e..7fb49966d 100644 --- a/lib/icp-canister/src/lib.rs +++ b/lib/icp-canister/src/lib.rs @@ -1,5 +1,9 @@ +use std::path::{Path, PathBuf}; + use serde::Deserialize; -use snafu::{ResultExt, Snafu}; +use snafu::{Snafu, ensure}; + +use icp_fs::fs::{ReadFileError, read}; /// Configuration for a Rust-based canister build adapter. #[derive(Debug, Deserialize)] @@ -76,8 +80,17 @@ pub struct CanisterManifest { } impl CanisterManifest { - pub fn from_bytes>(bytes: B) -> Result { - let cm: CanisterManifest = serde_yaml::from_slice(bytes.as_ref()).context(ParseSnafu)?; + pub fn from_file>(path: P) -> Result { + let path = path.as_ref(); + + // Check existence + ensure!(path.exists(), NotFoundSnafu { path }); + + // Read + let bytes = read(path)?; + + // Parse + let cm: CanisterManifest = serde_yaml::from_slice(bytes.as_ref())?; Ok(cm) } @@ -85,6 +98,12 @@ impl CanisterManifest { #[derive(Debug, Snafu)] pub enum CanisterManifestError { - #[snafu(display("failed to parse canister manifest: {}", source))] + #[snafu(display("canister manifest not found: {path:?}"))] + NotFound { path: PathBuf }, + + #[snafu(transparent)] Parse { source: serde_yaml::Error }, + + #[snafu(transparent)] + ReadFile { source: ReadFileError }, } diff --git a/lib/icp-project/Cargo.toml b/lib/icp-project/Cargo.toml index 4715bf547..19bd98f0b 100644 --- a/lib/icp-project/Cargo.toml +++ b/lib/icp-project/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] glob = { workspace = true } icp-canister = { path = "../icp-canister" } +icp-fs = { "path" = "../icp-fs" } icp-network = { path = "../icp-network" } serde = { workspace = true } serde_json = { workspace = true } diff --git a/lib/icp-project/src/lib.rs b/lib/icp-project/src/lib.rs index 2e48b1804..41eacb025 100644 --- a/lib/icp-project/src/lib.rs +++ b/lib/icp-project/src/lib.rs @@ -1,6 +1,9 @@ +use std::path::{Path, PathBuf}; + use serde::Deserialize; -use snafu::{OptionExt, ResultExt, Snafu}; -use std::path::PathBuf; +use snafu::{OptionExt, Snafu, ensure}; + +use icp_fs::fs::{ReadFileError, read}; /// Represents the manifest for an ICP project, typically loaded from `icp.yaml`. /// A project is a repository or directory grouping related canisters and network definitions. @@ -18,8 +21,17 @@ pub struct ProjectManifest { } impl ProjectManifest { - pub fn from_bytes>(bytes: B) -> Result { - let mut pm: ProjectManifest = serde_yaml::from_slice(bytes.as_ref()).context(ParseSnafu)?; + pub fn from_file>(path: P) -> Result { + let path = path.as_ref(); + + // Check existence + ensure!(path.exists(), NotFoundSnafu { path }); + + // Load + let bytes = read(path)?; + + // Parse + let mut pm: ProjectManifest = serde_yaml::from_slice(bytes.as_ref())?; // Project canisters let mut cs = Vec::new(); @@ -48,7 +60,10 @@ impl ProjectManifest { #[derive(Debug, Snafu)] pub enum ProjectManifestError { - #[snafu(display("failed to parse project manifest: {}", source))] + #[snafu(display("project manifest not found: {path:?}"))] + NotFound { path: PathBuf }, + + #[snafu(transparent)] Parse { source: serde_yaml::Error }, #[snafu(display("invalid UTF-8 in canister path"))] @@ -59,4 +74,7 @@ pub enum ProjectManifestError { #[snafu(transparent)] GlobWalk { source: glob::GlobError }, + + #[snafu(transparent)] + ReadFile { source: ReadFileError }, } From f3b4e96f207d0e76cccf777fdbf575632d6280f4 Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Fri, 30 May 2025 09:09:14 -0400 Subject: [PATCH 09/33] rename error for clarity --- bin/icp-cli/src/commands/build.rs | 8 ++++---- lib/icp-canister/src/lib.rs | 4 ++-- lib/icp-project/src/lib.rs | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bin/icp-cli/src/commands/build.rs b/bin/icp-cli/src/commands/build.rs index 366b47ced..db04c5b71 100644 --- a/bin/icp-cli/src/commands/build.rs +++ b/bin/icp-cli/src/commands/build.rs @@ -3,8 +3,8 @@ use std::path::PathBuf; use clap::Parser; use snafu::{ResultExt, Snafu}; -use icp_canister::{CanisterManifest, CanisterManifestError}; -use icp_project::{ProjectManifest, ProjectManifestError}; +use icp_canister::{CanisterManifest, LoadCanisterManifestError}; +use icp_project::{LoadProjectManifestError, ProjectManifest}; use crate::project::structure::ProjectDirectoryStructure; @@ -43,13 +43,13 @@ pub enum BuildCommandError { #[snafu(display("failed to load project manifest: {path:?}"))] ProjectLoad { - source: ProjectManifestError, + source: LoadProjectManifestError, path: PathBuf, }, #[snafu(display("failed to load canister manifest: {path:?}"))] CanisterLoad { - source: CanisterManifestError, + source: LoadCanisterManifestError, path: PathBuf, }, } diff --git a/lib/icp-canister/src/lib.rs b/lib/icp-canister/src/lib.rs index 7fb49966d..cbde551c0 100644 --- a/lib/icp-canister/src/lib.rs +++ b/lib/icp-canister/src/lib.rs @@ -80,7 +80,7 @@ pub struct CanisterManifest { } impl CanisterManifest { - pub fn from_file>(path: P) -> Result { + pub fn from_file>(path: P) -> Result { let path = path.as_ref(); // Check existence @@ -97,7 +97,7 @@ impl CanisterManifest { } #[derive(Debug, Snafu)] -pub enum CanisterManifestError { +pub enum LoadCanisterManifestError { #[snafu(display("canister manifest not found: {path:?}"))] NotFound { path: PathBuf }, diff --git a/lib/icp-project/src/lib.rs b/lib/icp-project/src/lib.rs index 41eacb025..3888e9e82 100644 --- a/lib/icp-project/src/lib.rs +++ b/lib/icp-project/src/lib.rs @@ -21,7 +21,7 @@ pub struct ProjectManifest { } impl ProjectManifest { - pub fn from_file>(path: P) -> Result { + pub fn from_file>(path: P) -> Result { let path = path.as_ref(); // Check existence @@ -59,7 +59,7 @@ impl ProjectManifest { } #[derive(Debug, Snafu)] -pub enum ProjectManifestError { +pub enum LoadProjectManifestError { #[snafu(display("project manifest not found: {path:?}"))] NotFound { path: PathBuf }, From 5a905d6145b8fbb559ca0129f415abb3b72b0b59 Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Fri, 30 May 2025 10:21:27 -0400 Subject: [PATCH 10/33] specify path in certain errors --- bin/icp-cli/src/commands/build.rs | 12 +++++------ lib/icp-canister/src/lib.rs | 14 ++++++++----- lib/icp-project/src/lib.rs | 35 +++++++++++++++++++++---------- 3 files changed, 39 insertions(+), 22 deletions(-) diff --git a/bin/icp-cli/src/commands/build.rs b/bin/icp-cli/src/commands/build.rs index db04c5b71..0dc1a79ff 100644 --- a/bin/icp-cli/src/commands/build.rs +++ b/bin/icp-cli/src/commands/build.rs @@ -12,20 +12,20 @@ use crate::project::structure::ProjectDirectoryStructure; pub struct Cmd; pub async fn dispatch(_cmd: Cmd) -> Result<(), BuildCommandError> { - let mpath = ProjectDirectoryStructure::find() + let path = ProjectDirectoryStructure::find() .ok_or(BuildCommandError::ProjectNotFound)? .root() .join("icp.yaml"); - let pm = ProjectManifest::from_file(&mpath).context(ProjectLoadSnafu { path: mpath })?; + let pm = ProjectManifest::from_file(&path).context(ProjectLoadSnafu { path })?; // List canisters in project let mut cs = Vec::new(); for c in pm.canisters { - let mpath = c.join("canister.yaml"); + let path = c.join("canister.yaml"); - let cm = CanisterManifest::from_file(&mpath).context(CanisterLoadSnafu { path: mpath })?; + let cm = CanisterManifest::from_file(&path).context(CanisterLoadSnafu { path })?; cs.push(cm); } @@ -41,13 +41,13 @@ pub enum BuildCommandError { #[snafu(display("no project (icp.yaml) found in current directory or its parents"))] ProjectNotFound, - #[snafu(display("failed to load project manifest: {path:?}"))] + #[snafu(display("failed to load project manifest: {}", path.display()))] ProjectLoad { source: LoadProjectManifestError, path: PathBuf, }, - #[snafu(display("failed to load canister manifest: {path:?}"))] + #[snafu(display("failed to load canister manifest: {}", path.display()))] CanisterLoad { source: LoadCanisterManifestError, path: PathBuf, diff --git a/lib/icp-canister/src/lib.rs b/lib/icp-canister/src/lib.rs index cbde551c0..7aeb24405 100644 --- a/lib/icp-canister/src/lib.rs +++ b/lib/icp-canister/src/lib.rs @@ -1,7 +1,7 @@ use std::path::{Path, PathBuf}; use serde::Deserialize; -use snafu::{Snafu, ensure}; +use snafu::{ResultExt, Snafu, ensure}; use icp_fs::fs::{ReadFileError, read}; @@ -90,7 +90,8 @@ impl CanisterManifest { let bytes = read(path)?; // Parse - let cm: CanisterManifest = serde_yaml::from_slice(bytes.as_ref())?; + let cm: CanisterManifest = + serde_yaml::from_slice(bytes.as_ref()).context(ParseSnafu { path })?; Ok(cm) } @@ -98,11 +99,14 @@ impl CanisterManifest { #[derive(Debug, Snafu)] pub enum LoadCanisterManifestError { - #[snafu(display("canister manifest not found: {path:?}"))] + #[snafu(display("canister manifest not found: {}", path.display()))] NotFound { path: PathBuf }, - #[snafu(transparent)] - Parse { source: serde_yaml::Error }, + #[snafu(display("failed to parse {}", path.display()))] + Parse { + source: serde_yaml::Error, + path: PathBuf, + }, #[snafu(transparent)] ReadFile { source: ReadFileError }, diff --git a/lib/icp-project/src/lib.rs b/lib/icp-project/src/lib.rs index 3888e9e82..6a4cb92ab 100644 --- a/lib/icp-project/src/lib.rs +++ b/lib/icp-project/src/lib.rs @@ -1,7 +1,7 @@ use std::path::{Path, PathBuf}; use serde::Deserialize; -use snafu::{OptionExt, Snafu, ensure}; +use snafu::{OptionExt, ResultExt, Snafu, ensure}; use icp_fs::fs::{ReadFileError, read}; @@ -31,14 +31,20 @@ impl ProjectManifest { let bytes = read(path)?; // Parse - let mut pm: ProjectManifest = serde_yaml::from_slice(bytes.as_ref())?; + let mut pm: ProjectManifest = + serde_yaml::from_slice(bytes.as_ref()).context(ParseSnafu { path })?; // Project canisters let mut cs = Vec::new(); for pattern in pm.canisters { - let pattern = pattern.to_str().context(InvalidPathUtf8Snafu)?; - let matches = glob::glob(pattern)?; + let patcpy = pattern.clone(); + + let pattern = pattern + .to_str() + .context(InvalidPathUtf8Snafu { pattern: patcpy })?; + + let matches = glob::glob(pattern).context(GlobPatternSnafu { pattern })?; for path in matches { let path = path?; @@ -60,18 +66,25 @@ impl ProjectManifest { #[derive(Debug, Snafu)] pub enum LoadProjectManifestError { - #[snafu(display("project manifest not found: {path:?}"))] + #[snafu(display("project manifest not found: {}", path.display()))] NotFound { path: PathBuf }, - #[snafu(transparent)] - Parse { source: serde_yaml::Error }, + #[snafu(display("failed to parse {}", path.display()))] + Parse { + source: serde_yaml::Error, + path: PathBuf, + }, - #[snafu(display("invalid UTF-8 in canister path"))] - InvalidPathUtf8, + #[snafu(display("invalid UTF-8 in canister path pattern {}", pattern.display()))] + InvalidPathUtf8 { pattern: PathBuf }, - #[snafu(transparent)] - GlobPattern { source: glob::PatternError }, + #[snafu(display("failed to glob pattern {pattern}"))] + GlobPattern { + source: glob::PatternError, + pattern: String, + }, + /// GlobWalk is transparent because `glob::GlobError` already contains the path. #[snafu(transparent)] GlobWalk { source: glob::GlobError }, From 63b87404e1c804e502a596387973e8c47f592ef8 Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Fri, 30 May 2025 10:24:14 -0400 Subject: [PATCH 11/33] remove assets canister variant until it can be further discussed --- lib/icp-canister/src/lib.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/lib/icp-canister/src/lib.rs b/lib/icp-canister/src/lib.rs index 7aeb24405..13f1de46e 100644 --- a/lib/icp-canister/src/lib.rs +++ b/lib/icp-canister/src/lib.rs @@ -28,13 +28,6 @@ pub struct CustomAdapter { pub script: String, } -/// Configuration for an asset canister used to serve static content. -#[derive(Debug, Deserialize)] -pub struct AssetsAdapter { - /// Directory containing the static assets to include in the canister. - pub source: String, -} - /// Identifies the type of adapter used to build the canister, /// along with its configuration. /// @@ -56,9 +49,6 @@ pub enum Adapter { /// A canister built using a custom script or command. Custom(CustomAdapter), - - /// A static asset canister. - Assets(AssetsAdapter), } /// Describes how the canister should be built into WebAssembly, From 8c0ffbbc2a335b46682611535ae27b6fb0bee052 Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Fri, 30 May 2025 10:31:50 -0400 Subject: [PATCH 12/33] specify explicit defaults for project manifest --- lib/icp-project/src/lib.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/icp-project/src/lib.rs b/lib/icp-project/src/lib.rs index 6a4cb92ab..a54ba0933 100644 --- a/lib/icp-project/src/lib.rs +++ b/lib/icp-project/src/lib.rs @@ -5,18 +5,30 @@ use snafu::{OptionExt, ResultExt, Snafu, ensure}; use icp_fs::fs::{ReadFileError, read}; +/// Provides the default glob pattern for locating canister manifests +/// when the `canisters` field is not explicitly specified in the YAML. +fn default_canisters() -> Vec { + ["canisters/*"].iter().map(PathBuf::from).collect() +} + +/// Provides the default glob pattern for locating network definition files +/// when the `networks` field is not explicitly specified in the YAML. +fn default_networks() -> Vec { + ["networks/*"].iter().map(PathBuf::from).collect() +} + /// Represents the manifest for an ICP project, typically loaded from `icp.yaml`. /// A project is a repository or directory grouping related canisters and network definitions. #[derive(Debug, Deserialize)] pub struct ProjectManifest { /// List of canister manifests belonging to this project. /// Supports glob patterns to specify multiple canister YAML files. - #[serde(default)] + #[serde(default = "default_canisters")] pub canisters: Vec, /// List of network definition files relevant to the project. /// Supports glob patterns to reference multiple network config files. - #[serde(default)] + #[serde(default = "default_networks")] pub networks: Vec, } From 0ea957b770a234b8968191109db5b623066f7e7b Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Fri, 30 May 2025 10:34:41 -0400 Subject: [PATCH 13/33] remove redundant check for file existence --- lib/icp-project/src/lib.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/icp-project/src/lib.rs b/lib/icp-project/src/lib.rs index a54ba0933..3511ae219 100644 --- a/lib/icp-project/src/lib.rs +++ b/lib/icp-project/src/lib.rs @@ -1,7 +1,7 @@ use std::path::{Path, PathBuf}; use serde::Deserialize; -use snafu::{OptionExt, ResultExt, Snafu, ensure}; +use snafu::{OptionExt, ResultExt, Snafu}; use icp_fs::fs::{ReadFileError, read}; @@ -36,9 +36,6 @@ impl ProjectManifest { pub fn from_file>(path: P) -> Result { let path = path.as_ref(); - // Check existence - ensure!(path.exists(), NotFoundSnafu { path }); - // Load let bytes = read(path)?; @@ -78,9 +75,6 @@ impl ProjectManifest { #[derive(Debug, Snafu)] pub enum LoadProjectManifestError { - #[snafu(display("project manifest not found: {}", path.display()))] - NotFound { path: PathBuf }, - #[snafu(display("failed to parse {}", path.display()))] Parse { source: serde_yaml::Error, From dab4ac291fd7f99479bd02a5ee9117ae7aa2ffbf Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Fri, 30 May 2025 10:47:09 -0400 Subject: [PATCH 14/33] create a load_yaml_file function and use it in icp-project and icp-canister --- Cargo.lock | 5 +---- lib/icp-canister/Cargo.toml | 2 -- lib/icp-canister/src/lib.rs | 28 ++++++---------------------- lib/icp-fs/Cargo.toml | 1 + lib/icp-fs/src/lib.rs | 1 + lib/icp-fs/src/yaml.rs | 25 +++++++++++++++++++++++++ lib/icp-project/Cargo.toml | 2 -- lib/icp-project/src/lib.rs | 18 ++++-------------- 8 files changed, 38 insertions(+), 44 deletions(-) create mode 100644 lib/icp-fs/src/yaml.rs diff --git a/Cargo.lock b/Cargo.lock index 01396a651..e99fd5226 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1207,8 +1207,6 @@ version = "0.1.0" dependencies = [ "icp-fs", "serde", - "serde_json", - "serde_yaml", "snafu", ] @@ -1235,6 +1233,7 @@ version = "0.1.0" dependencies = [ "serde", "serde_json", + "serde_yaml", "snafu", ] @@ -1270,8 +1269,6 @@ dependencies = [ "icp-fs", "icp-network", "serde", - "serde_json", - "serde_yaml", "snafu", ] diff --git a/lib/icp-canister/Cargo.toml b/lib/icp-canister/Cargo.toml index 28fac5cc8..593177487 100644 --- a/lib/icp-canister/Cargo.toml +++ b/lib/icp-canister/Cargo.toml @@ -6,6 +6,4 @@ edition = "2024" [dependencies] icp-fs = { "path" = "../icp-fs" } serde = { workspace = true } -serde_json = { workspace = true } -serde_yaml = { workspace = true } snafu = { workspace = true } diff --git a/lib/icp-canister/src/lib.rs b/lib/icp-canister/src/lib.rs index 13f1de46e..552c853fb 100644 --- a/lib/icp-canister/src/lib.rs +++ b/lib/icp-canister/src/lib.rs @@ -1,9 +1,9 @@ -use std::path::{Path, PathBuf}; +use std::path::Path; use serde::Deserialize; -use snafu::{ResultExt, Snafu, ensure}; +use snafu::Snafu; -use icp_fs::fs::{ReadFileError, read}; +use icp_fs::yaml::{LoadYamlFileError, load_yaml_file}; /// Configuration for a Rust-based canister build adapter. #[derive(Debug, Deserialize)] @@ -73,15 +73,8 @@ impl CanisterManifest { pub fn from_file>(path: P) -> Result { let path = path.as_ref(); - // Check existence - ensure!(path.exists(), NotFoundSnafu { path }); - - // Read - let bytes = read(path)?; - - // Parse - let cm: CanisterManifest = - serde_yaml::from_slice(bytes.as_ref()).context(ParseSnafu { path })?; + // Load + let cm: CanisterManifest = load_yaml_file(path)?; Ok(cm) } @@ -89,15 +82,6 @@ impl CanisterManifest { #[derive(Debug, Snafu)] pub enum LoadCanisterManifestError { - #[snafu(display("canister manifest not found: {}", path.display()))] - NotFound { path: PathBuf }, - - #[snafu(display("failed to parse {}", path.display()))] - Parse { - source: serde_yaml::Error, - path: PathBuf, - }, - #[snafu(transparent)] - ReadFile { source: ReadFileError }, + Parse { source: LoadYamlFileError }, } diff --git a/lib/icp-fs/Cargo.toml b/lib/icp-fs/Cargo.toml index f27149188..b58755192 100644 --- a/lib/icp-fs/Cargo.toml +++ b/lib/icp-fs/Cargo.toml @@ -6,4 +6,5 @@ edition = "2024" [dependencies] serde = { workspace = true } serde_json = { workspace = true } +serde_yaml = { workspace = true } snafu = { workspace = true } diff --git a/lib/icp-fs/src/lib.rs b/lib/icp-fs/src/lib.rs index ba52be046..e1d65114a 100644 --- a/lib/icp-fs/src/lib.rs +++ b/lib/icp-fs/src/lib.rs @@ -1,2 +1,3 @@ pub mod fs; pub mod json; +pub mod yaml; diff --git a/lib/icp-fs/src/yaml.rs b/lib/icp-fs/src/yaml.rs new file mode 100644 index 000000000..940d573d7 --- /dev/null +++ b/lib/icp-fs/src/yaml.rs @@ -0,0 +1,25 @@ +use std::path::{Path, PathBuf}; + +use snafu::prelude::*; + +use crate::fs::{ReadFileError, read}; + +#[derive(Snafu, Debug)] +pub enum LoadYamlFileError { + #[snafu(display("failed to parse {} as yaml", path.display()))] + Parse { + path: PathBuf, + source: serde_yaml::Error, + }, + + #[snafu(transparent)] + Read { source: ReadFileError }, +} + +pub fn load_yaml_file serde::de::Deserialize<'a>>( + path: &Path, +) -> Result { + let content = read(path)?; + + serde_yaml::from_slice(content.as_ref()).context(ParseSnafu { path }) +} diff --git a/lib/icp-project/Cargo.toml b/lib/icp-project/Cargo.toml index 19bd98f0b..20605353a 100644 --- a/lib/icp-project/Cargo.toml +++ b/lib/icp-project/Cargo.toml @@ -9,6 +9,4 @@ icp-canister = { path = "../icp-canister" } icp-fs = { "path" = "../icp-fs" } icp-network = { path = "../icp-network" } serde = { workspace = true } -serde_json = { workspace = true } -serde_yaml = { workspace = true } snafu = { workspace = true } diff --git a/lib/icp-project/src/lib.rs b/lib/icp-project/src/lib.rs index 3511ae219..204aca4df 100644 --- a/lib/icp-project/src/lib.rs +++ b/lib/icp-project/src/lib.rs @@ -3,7 +3,7 @@ use std::path::{Path, PathBuf}; use serde::Deserialize; use snafu::{OptionExt, ResultExt, Snafu}; -use icp_fs::fs::{ReadFileError, read}; +use icp_fs::yaml::{LoadYamlFileError, load_yaml_file}; /// Provides the default glob pattern for locating canister manifests /// when the `canisters` field is not explicitly specified in the YAML. @@ -37,11 +37,7 @@ impl ProjectManifest { let path = path.as_ref(); // Load - let bytes = read(path)?; - - // Parse - let mut pm: ProjectManifest = - serde_yaml::from_slice(bytes.as_ref()).context(ParseSnafu { path })?; + let mut pm: ProjectManifest = load_yaml_file(path)?; // Project canisters let mut cs = Vec::new(); @@ -75,11 +71,8 @@ impl ProjectManifest { #[derive(Debug, Snafu)] pub enum LoadProjectManifestError { - #[snafu(display("failed to parse {}", path.display()))] - Parse { - source: serde_yaml::Error, - path: PathBuf, - }, + #[snafu(transparent)] + Parse { source: LoadYamlFileError }, #[snafu(display("invalid UTF-8 in canister path pattern {}", pattern.display()))] InvalidPathUtf8 { pattern: PathBuf }, @@ -93,7 +86,4 @@ pub enum LoadProjectManifestError { /// GlobWalk is transparent because `glob::GlobError` already contains the path. #[snafu(transparent)] GlobWalk { source: glob::GlobError }, - - #[snafu(transparent)] - ReadFile { source: ReadFileError }, } From a6eb5babd58f8db911165982230f52a9bc83e54c Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Fri, 30 May 2025 11:05:00 -0400 Subject: [PATCH 15/33] fix ProjectDirectory usage --- bin/icp-cli/src/commands/build.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/icp-cli/src/commands/build.rs b/bin/icp-cli/src/commands/build.rs index 0dc1a79ff..a6ccb5f2a 100644 --- a/bin/icp-cli/src/commands/build.rs +++ b/bin/icp-cli/src/commands/build.rs @@ -6,14 +6,15 @@ use snafu::{ResultExt, Snafu}; use icp_canister::{CanisterManifest, LoadCanisterManifestError}; use icp_project::{LoadProjectManifestError, ProjectManifest}; -use crate::project::structure::ProjectDirectoryStructure; +use crate::project::directory::ProjectDirectory; #[derive(Parser, Debug)] pub struct Cmd; pub async fn dispatch(_cmd: Cmd) -> Result<(), BuildCommandError> { - let path = ProjectDirectoryStructure::find() + let path = ProjectDirectory::find() .ok_or(BuildCommandError::ProjectNotFound)? + .structure() .root() .join("icp.yaml"); From 54e54bfb4e306e00967dfe9e2acf10ff0fad8935 Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Fri, 30 May 2025 12:10:14 -0400 Subject: [PATCH 16/33] get project manifest path from project directory object --- bin/icp-cli/src/commands/build.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bin/icp-cli/src/commands/build.rs b/bin/icp-cli/src/commands/build.rs index a6ccb5f2a..f87d63f36 100644 --- a/bin/icp-cli/src/commands/build.rs +++ b/bin/icp-cli/src/commands/build.rs @@ -15,8 +15,7 @@ pub async fn dispatch(_cmd: Cmd) -> Result<(), BuildCommandError> { let path = ProjectDirectory::find() .ok_or(BuildCommandError::ProjectNotFound)? .structure() - .root() - .join("icp.yaml"); + .project_yaml_path(); let pm = ProjectManifest::from_file(&path).context(ProjectLoadSnafu { path })?; From f3201f5c0c4e442a21b11cf4971cf4b4cc6e4e0a Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Fri, 30 May 2025 12:11:36 -0400 Subject: [PATCH 17/33] remove redundant path in error --- bin/icp-cli/src/commands/build.rs | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/bin/icp-cli/src/commands/build.rs b/bin/icp-cli/src/commands/build.rs index f87d63f36..f5ded4e01 100644 --- a/bin/icp-cli/src/commands/build.rs +++ b/bin/icp-cli/src/commands/build.rs @@ -17,7 +17,7 @@ pub async fn dispatch(_cmd: Cmd) -> Result<(), BuildCommandError> { .structure() .project_yaml_path(); - let pm = ProjectManifest::from_file(&path).context(ProjectLoadSnafu { path })?; + let pm = ProjectManifest::from_file(&path)?; // List canisters in project let mut cs = Vec::new(); @@ -25,7 +25,7 @@ pub async fn dispatch(_cmd: Cmd) -> Result<(), BuildCommandError> { for c in pm.canisters { let path = c.join("canister.yaml"); - let cm = CanisterManifest::from_file(&path).context(CanisterLoadSnafu { path })?; + let cm = CanisterManifest::from_file(&path)?; cs.push(cm); } @@ -41,15 +41,9 @@ pub enum BuildCommandError { #[snafu(display("no project (icp.yaml) found in current directory or its parents"))] ProjectNotFound, - #[snafu(display("failed to load project manifest: {}", path.display()))] - ProjectLoad { - source: LoadProjectManifestError, - path: PathBuf, - }, - - #[snafu(display("failed to load canister manifest: {}", path.display()))] - CanisterLoad { - source: LoadCanisterManifestError, - path: PathBuf, - }, + #[snafu(transparent)] + ProjectLoad { source: LoadProjectManifestError }, + + #[snafu(transparent)] + CanisterLoad { source: LoadCanisterManifestError }, } From e47f35f4edb23a3e2b6a32979a97a24b0a3bc3ac Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Fri, 30 May 2025 12:14:47 -0400 Subject: [PATCH 18/33] put structs into model.rs --- lib/icp-canister/src/lib.rs | 24 ++---------------------- lib/icp-canister/src/model.rs | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 22 deletions(-) create mode 100644 lib/icp-canister/src/model.rs diff --git a/lib/icp-canister/src/lib.rs b/lib/icp-canister/src/lib.rs index 552c853fb..b0344fd61 100644 --- a/lib/icp-canister/src/lib.rs +++ b/lib/icp-canister/src/lib.rs @@ -5,28 +5,8 @@ use snafu::Snafu; use icp_fs::yaml::{LoadYamlFileError, load_yaml_file}; -/// Configuration for a Rust-based canister build adapter. -#[derive(Debug, Deserialize)] -pub struct RustAdapter { - /// The name of the Cargo package to build. - pub package: String, -} - -/// Configuration for a Motoko-based canister build adapter. -#[derive(Debug, Deserialize)] -pub struct MotokoAdapter { - /// Optional path to the main Motoko source file. - /// If omitted, a default like `main.mo` may be assumed. - #[serde(default)] - pub main: Option, -} - -/// Configuration for a custom canister build adapter. -#[derive(Debug, Deserialize)] -pub struct CustomAdapter { - /// Path to a script or executable used to build the canister. - pub script: String, -} +mod model; +use model::{CustomAdapter, MotokoAdapter, RustAdapter}; /// Identifies the type of adapter used to build the canister, /// along with its configuration. diff --git a/lib/icp-canister/src/model.rs b/lib/icp-canister/src/model.rs new file mode 100644 index 000000000..65edac7cd --- /dev/null +++ b/lib/icp-canister/src/model.rs @@ -0,0 +1,24 @@ +use serde::Deserialize; + +/// Configuration for a Rust-based canister build adapter. +#[derive(Debug, Deserialize)] +pub struct RustAdapter { + /// The name of the Cargo package to build. + pub package: String, +} + +/// Configuration for a Motoko-based canister build adapter. +#[derive(Debug, Deserialize)] +pub struct MotokoAdapter { + /// Optional path to the main Motoko source file. + /// If omitted, a default like `main.mo` may be assumed. + #[serde(default)] + pub main: Option, +} + +/// Configuration for a custom canister build adapter. +#[derive(Debug, Deserialize)] +pub struct CustomAdapter { + /// Path to a script or executable used to build the canister. + pub script: String, +} From a2f998d699643dd77cf4948b3f3345ed002547a2 Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Fri, 30 May 2025 12:26:44 -0400 Subject: [PATCH 19/33] update build code paths to use camino strings --- Cargo.lock | 2 ++ bin/icp-cli/src/commands/build.rs | 11 ++++---- lib/icp-canister/Cargo.toml | 1 + lib/icp-canister/src/lib.rs | 5 ++-- lib/icp-fs/src/yaml.rs | 10 +++---- lib/icp-project/Cargo.toml | 1 + lib/icp-project/src/lib.rs | 47 +++++++++++++++---------------- 7 files changed, 39 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index da0f5416e..267065a43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1224,6 +1224,7 @@ dependencies = [ name = "icp-canister" version = "0.1.0" dependencies = [ + "camino", "icp-fs", "serde", "snafu", @@ -1286,6 +1287,7 @@ dependencies = [ name = "icp-project" version = "0.1.0" dependencies = [ + "camino", "glob", "icp-canister", "icp-fs", diff --git a/bin/icp-cli/src/commands/build.rs b/bin/icp-cli/src/commands/build.rs index f5ded4e01..5341e4d8a 100644 --- a/bin/icp-cli/src/commands/build.rs +++ b/bin/icp-cli/src/commands/build.rs @@ -1,18 +1,16 @@ -use std::path::PathBuf; - use clap::Parser; -use snafu::{ResultExt, Snafu}; +use snafu::Snafu; use icp_canister::{CanisterManifest, LoadCanisterManifestError}; use icp_project::{LoadProjectManifestError, ProjectManifest}; -use crate::project::directory::ProjectDirectory; +use crate::project::directory::{FindProjectError, ProjectDirectory}; #[derive(Parser, Debug)] pub struct Cmd; pub async fn dispatch(_cmd: Cmd) -> Result<(), BuildCommandError> { - let path = ProjectDirectory::find() + let path = ProjectDirectory::find()? .ok_or(BuildCommandError::ProjectNotFound)? .structure() .project_yaml_path(); @@ -38,6 +36,9 @@ pub async fn dispatch(_cmd: Cmd) -> Result<(), BuildCommandError> { #[derive(Debug, Snafu)] pub enum BuildCommandError { + #[snafu(transparent)] + FindProjectError { source: FindProjectError }, + #[snafu(display("no project (icp.yaml) found in current directory or its parents"))] ProjectNotFound, diff --git a/lib/icp-canister/Cargo.toml b/lib/icp-canister/Cargo.toml index 593177487..ac7dd7277 100644 --- a/lib/icp-canister/Cargo.toml +++ b/lib/icp-canister/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +camino = { workspace = true } icp-fs = { "path" = "../icp-fs" } serde = { workspace = true } snafu = { workspace = true } diff --git a/lib/icp-canister/src/lib.rs b/lib/icp-canister/src/lib.rs index b0344fd61..88ebf2b62 100644 --- a/lib/icp-canister/src/lib.rs +++ b/lib/icp-canister/src/lib.rs @@ -1,5 +1,4 @@ -use std::path::Path; - +use camino::Utf8Path; use serde::Deserialize; use snafu::Snafu; @@ -50,7 +49,7 @@ pub struct CanisterManifest { } impl CanisterManifest { - pub fn from_file>(path: P) -> Result { + pub fn from_file>(path: P) -> Result { let path = path.as_ref(); // Load diff --git a/lib/icp-fs/src/yaml.rs b/lib/icp-fs/src/yaml.rs index 940d573d7..3e2413486 100644 --- a/lib/icp-fs/src/yaml.rs +++ b/lib/icp-fs/src/yaml.rs @@ -1,14 +1,13 @@ -use std::path::{Path, PathBuf}; - +use camino::{Utf8Path, Utf8PathBuf}; use snafu::prelude::*; use crate::fs::{ReadFileError, read}; #[derive(Snafu, Debug)] pub enum LoadYamlFileError { - #[snafu(display("failed to parse {} as yaml", path.display()))] + #[snafu(display("failed to parse {path} as yaml"))] Parse { - path: PathBuf, + path: Utf8PathBuf, source: serde_yaml::Error, }, @@ -17,8 +16,9 @@ pub enum LoadYamlFileError { } pub fn load_yaml_file serde::de::Deserialize<'a>>( - path: &Path, + path: impl AsRef, ) -> Result { + let path = path.as_ref(); let content = read(path)?; serde_yaml::from_slice(content.as_ref()).context(ParseSnafu { path }) diff --git a/lib/icp-project/Cargo.toml b/lib/icp-project/Cargo.toml index 20605353a..8535bd879 100644 --- a/lib/icp-project/Cargo.toml +++ b/lib/icp-project/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] +camino = { workspace = true } glob = { workspace = true } icp-canister = { path = "../icp-canister" } icp-fs = { "path" = "../icp-fs" } diff --git a/lib/icp-project/src/lib.rs b/lib/icp-project/src/lib.rs index 204aca4df..186b12dde 100644 --- a/lib/icp-project/src/lib.rs +++ b/lib/icp-project/src/lib.rs @@ -1,20 +1,19 @@ -use std::path::{Path, PathBuf}; - +use camino::{Utf8Path, Utf8PathBuf}; use serde::Deserialize; -use snafu::{OptionExt, ResultExt, Snafu}; +use snafu::{ResultExt, Snafu}; use icp_fs::yaml::{LoadYamlFileError, load_yaml_file}; /// Provides the default glob pattern for locating canister manifests /// when the `canisters` field is not explicitly specified in the YAML. -fn default_canisters() -> Vec { - ["canisters/*"].iter().map(PathBuf::from).collect() +fn default_canisters() -> Vec { + ["canisters/*"].iter().map(Utf8PathBuf::from).collect() } /// Provides the default glob pattern for locating network definition files /// when the `networks` field is not explicitly specified in the YAML. -fn default_networks() -> Vec { - ["networks/*"].iter().map(PathBuf::from).collect() +fn default_networks() -> Vec { + ["networks/*"].iter().map(Utf8PathBuf::from).collect() } /// Represents the manifest for an ICP project, typically loaded from `icp.yaml`. @@ -24,35 +23,31 @@ pub struct ProjectManifest { /// List of canister manifests belonging to this project. /// Supports glob patterns to specify multiple canister YAML files. #[serde(default = "default_canisters")] - pub canisters: Vec, + pub canisters: Vec, /// List of network definition files relevant to the project. /// Supports glob patterns to reference multiple network config files. #[serde(default = "default_networks")] - pub networks: Vec, + pub networks: Vec, } impl ProjectManifest { - pub fn from_file>(path: P) -> Result { - let path = path.as_ref(); + pub fn from_file>(path: P) -> Result { + let mpath = path.as_ref(); // Load - let mut pm: ProjectManifest = load_yaml_file(path)?; + let mut pm: ProjectManifest = load_yaml_file(mpath)?; // Project canisters let mut cs = Vec::new(); for pattern in pm.canisters { - let patcpy = pattern.clone(); - - let pattern = pattern - .to_str() - .context(InvalidPathUtf8Snafu { pattern: patcpy })?; + let matches = glob::glob(pattern.as_str()).context(GlobPatternSnafu { pattern })?; - let matches = glob::glob(pattern).context(GlobPatternSnafu { pattern })?; + for cpath in matches { + let cpath = cpath.context(GlobWalkSnafu { path: mpath })?; - for path in matches { - let path = path?; + let path: Utf8PathBuf = cpath.try_into()?; // Skip non-canister directories if !path.join("canister.yaml").exists() { @@ -74,8 +69,8 @@ pub enum LoadProjectManifestError { #[snafu(transparent)] Parse { source: LoadYamlFileError }, - #[snafu(display("invalid UTF-8 in canister path pattern {}", pattern.display()))] - InvalidPathUtf8 { pattern: PathBuf }, + #[snafu(transparent)] + InvalidPathUtf8 { source: camino::FromPathBufError }, #[snafu(display("failed to glob pattern {pattern}"))] GlobPattern { @@ -83,7 +78,9 @@ pub enum LoadProjectManifestError { pattern: String, }, - /// GlobWalk is transparent because `glob::GlobError` already contains the path. - #[snafu(transparent)] - GlobWalk { source: glob::GlobError }, + #[snafu(display("failed to glob pattern in {path}"))] + GlobWalk { + source: glob::GlobError, + path: Utf8PathBuf, + }, } From e5a45e1864aeb610d15171dcca630c1436648a57 Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Fri, 30 May 2025 15:30:51 -0400 Subject: [PATCH 20/33] fix globbing not working from non-project root directory --- lib/icp-project/src/lib.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/icp-project/src/lib.rs b/lib/icp-project/src/lib.rs index 186b12dde..fdf9feba1 100644 --- a/lib/icp-project/src/lib.rs +++ b/lib/icp-project/src/lib.rs @@ -1,6 +1,6 @@ use camino::{Utf8Path, Utf8PathBuf}; use serde::Deserialize; -use snafu::{ResultExt, Snafu}; +use snafu::{OptionExt, ResultExt, Snafu}; use icp_fs::yaml::{LoadYamlFileError, load_yaml_file}; @@ -42,7 +42,12 @@ impl ProjectManifest { let mut cs = Vec::new(); for pattern in pm.canisters { - let matches = glob::glob(pattern.as_str()).context(GlobPatternSnafu { pattern })?; + let mdir = mpath + .parent() + .context(ProjectDirectorySnafu { path: mpath })?; + + let matches = + glob::glob(mdir.join(&pattern).as_str()).context(GlobPatternSnafu { pattern })?; for cpath in matches { let cpath = cpath.context(GlobWalkSnafu { path: mpath })?; @@ -66,6 +71,9 @@ impl ProjectManifest { #[derive(Debug, Snafu)] pub enum LoadProjectManifestError { + #[snafu(display("failed to find project directory for project manifest {path}"))] + ProjectDirectory { path: Utf8PathBuf }, + #[snafu(transparent)] Parse { source: LoadYamlFileError }, From a67b8f236584d38f123939ef189f7f84514c889c Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Mon, 2 Jun 2025 12:48:16 -0400 Subject: [PATCH 21/33] move project structure and directory to icp-project module --- bin/icp-cli/src/commands/build.rs | 2 +- bin/icp-cli/src/commands/network/run.rs | 10 ++++------ bin/icp-cli/src/main.rs | 1 - bin/icp-cli/src/project.rs | 2 -- .../src/project => lib/icp-project/src}/directory.rs | 8 +++++--- lib/icp-project/src/lib.rs | 3 +++ .../src/project => lib/icp-project/src}/structure.rs | 0 7 files changed, 13 insertions(+), 13 deletions(-) delete mode 100644 bin/icp-cli/src/project.rs rename {bin/icp-cli/src/project => lib/icp-project/src}/directory.rs (95%) rename {bin/icp-cli/src/project => lib/icp-project/src}/structure.rs (100%) diff --git a/bin/icp-cli/src/commands/build.rs b/bin/icp-cli/src/commands/build.rs index 5341e4d8a..768f63e29 100644 --- a/bin/icp-cli/src/commands/build.rs +++ b/bin/icp-cli/src/commands/build.rs @@ -4,7 +4,7 @@ use snafu::Snafu; use icp_canister::{CanisterManifest, LoadCanisterManifestError}; use icp_project::{LoadProjectManifestError, ProjectManifest}; -use crate::project::directory::{FindProjectError, ProjectDirectory}; +use icp_project::directory::{FindProjectError, ProjectDirectory}; #[derive(Parser, Debug)] pub struct Cmd; diff --git a/bin/icp-cli/src/commands/network/run.rs b/bin/icp-cli/src/commands/network/run.rs index 7b34eee0f..0020afe3e 100644 --- a/bin/icp-cli/src/commands/network/run.rs +++ b/bin/icp-cli/src/commands/network/run.rs @@ -1,12 +1,10 @@ -use crate::project::directory::ProjectDirectory; -use crate::{ - commands::network::run::RunNetworkCommandError::ProjectNotFound, - project::directory::FindProjectError, -}; use clap::Parser; -use icp_network::{ManagedNetworkModel, RunNetworkError, run_network}; use snafu::Snafu; +use crate::commands::network::run::RunNetworkCommandError::ProjectNotFound; +use icp_network::{ManagedNetworkModel, RunNetworkError, run_network}; +use icp_project::directory::{FindProjectError, ProjectDirectory}; + /// Run the local network #[derive(Parser, Debug)] pub struct Cmd {} diff --git a/bin/icp-cli/src/main.rs b/bin/icp-cli/src/main.rs index 579157bb5..e87744e2b 100644 --- a/bin/icp-cli/src/main.rs +++ b/bin/icp-cli/src/main.rs @@ -2,7 +2,6 @@ use crate::commands::Cli; use clap::Parser; mod commands; -mod project; #[tokio::main] async fn main() { diff --git a/bin/icp-cli/src/project.rs b/bin/icp-cli/src/project.rs deleted file mode 100644 index 329ccc42c..000000000 --- a/bin/icp-cli/src/project.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod directory; -pub mod structure; diff --git a/bin/icp-cli/src/project/directory.rs b/lib/icp-project/src/directory.rs similarity index 95% rename from bin/icp-cli/src/project/directory.rs rename to lib/icp-project/src/directory.rs index 6413e74c7..d7babd174 100644 --- a/bin/icp-cli/src/project/directory.rs +++ b/lib/icp-project/src/directory.rs @@ -1,8 +1,10 @@ -use crate::project::structure::ProjectDirectoryStructure; +use std::io; + use camino::Utf8PathBuf; -use icp_network::NetworkDirectory; use snafu::{ResultExt, Snafu}; -use std::io; + +use crate::structure::ProjectDirectoryStructure; +use icp_network::NetworkDirectory; pub struct ProjectDirectory { structure: ProjectDirectoryStructure, diff --git a/lib/icp-project/src/lib.rs b/lib/icp-project/src/lib.rs index fdf9feba1..a29fcebb2 100644 --- a/lib/icp-project/src/lib.rs +++ b/lib/icp-project/src/lib.rs @@ -4,6 +4,9 @@ use snafu::{OptionExt, ResultExt, Snafu}; use icp_fs::yaml::{LoadYamlFileError, load_yaml_file}; +pub mod directory; +pub mod structure; + /// Provides the default glob pattern for locating canister manifests /// when the `canisters` field is not explicitly specified in the YAML. fn default_canisters() -> Vec { diff --git a/bin/icp-cli/src/project/structure.rs b/lib/icp-project/src/structure.rs similarity index 100% rename from bin/icp-cli/src/project/structure.rs rename to lib/icp-project/src/structure.rs From dcf6c8e9eedb01876cba7b5fe3d768bd11114eb4 Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Mon, 2 Jun 2025 12:58:49 -0400 Subject: [PATCH 22/33] extract knowledge of canister yaml path into project directory structure --- lib/icp-project/src/lib.rs | 6 +++++- lib/icp-project/src/structure.rs | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/icp-project/src/lib.rs b/lib/icp-project/src/lib.rs index a29fcebb2..f8353395b 100644 --- a/lib/icp-project/src/lib.rs +++ b/lib/icp-project/src/lib.rs @@ -3,6 +3,7 @@ use serde::Deserialize; use snafu::{OptionExt, ResultExt, Snafu}; use icp_fs::yaml::{LoadYamlFileError, load_yaml_file}; +use structure::ProjectDirectoryStructure; pub mod directory; pub mod structure; @@ -38,6 +39,9 @@ impl ProjectManifest { pub fn from_file>(path: P) -> Result { let mpath = path.as_ref(); + // Project + let pds = ProjectDirectoryStructure::new(mpath); + // Load let mut pm: ProjectManifest = load_yaml_file(mpath)?; @@ -58,7 +62,7 @@ impl ProjectManifest { let path: Utf8PathBuf = cpath.try_into()?; // Skip non-canister directories - if !path.join("canister.yaml").exists() { + if !pds.canister_yaml_path(&path).exists() { continue; } diff --git a/lib/icp-project/src/structure.rs b/lib/icp-project/src/structure.rs index 3f3b8853a..a548d0dfe 100644 --- a/lib/icp-project/src/structure.rs +++ b/lib/icp-project/src/structure.rs @@ -30,4 +30,8 @@ impl ProjectDirectoryStructure { pub fn network_root(&self, network_name: &str) -> Utf8PathBuf { self.work_dir().join("networks").join(network_name) } + + pub fn canister_yaml_path(&self, canister_dir: &Utf8Path) -> Utf8PathBuf { + self.root.join(canister_dir).join("canister.yaml") + } } From 372e3ab59e5b4fbc5e0907a6371ff5bc098eaf85 Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Tue, 3 Jun 2025 10:25:30 -0400 Subject: [PATCH 23/33] single group imports --- bin/icp-cli/src/commands/build.rs | 6 ++---- bin/icp-cli/src/commands/network/run.rs | 5 ++--- lib/icp-canister/src/lib.rs | 5 ++--- lib/icp-project/src/directory.rs | 8 +++----- lib/icp-project/src/lib.rs | 3 +-- 5 files changed, 10 insertions(+), 17 deletions(-) diff --git a/bin/icp-cli/src/commands/build.rs b/bin/icp-cli/src/commands/build.rs index 768f63e29..a482bb376 100644 --- a/bin/icp-cli/src/commands/build.rs +++ b/bin/icp-cli/src/commands/build.rs @@ -1,10 +1,8 @@ use clap::Parser; -use snafu::Snafu; - use icp_canister::{CanisterManifest, LoadCanisterManifestError}; -use icp_project::{LoadProjectManifestError, ProjectManifest}; - use icp_project::directory::{FindProjectError, ProjectDirectory}; +use icp_project::{LoadProjectManifestError, ProjectManifest}; +use snafu::Snafu; #[derive(Parser, Debug)] pub struct Cmd; diff --git a/bin/icp-cli/src/commands/network/run.rs b/bin/icp-cli/src/commands/network/run.rs index 0020afe3e..125330a45 100644 --- a/bin/icp-cli/src/commands/network/run.rs +++ b/bin/icp-cli/src/commands/network/run.rs @@ -1,9 +1,8 @@ -use clap::Parser; -use snafu::Snafu; - use crate::commands::network::run::RunNetworkCommandError::ProjectNotFound; +use clap::Parser; use icp_network::{ManagedNetworkModel, RunNetworkError, run_network}; use icp_project::directory::{FindProjectError, ProjectDirectory}; +use snafu::Snafu; /// Run the local network #[derive(Parser, Debug)] diff --git a/lib/icp-canister/src/lib.rs b/lib/icp-canister/src/lib.rs index 88ebf2b62..25844f7d0 100644 --- a/lib/icp-canister/src/lib.rs +++ b/lib/icp-canister/src/lib.rs @@ -1,11 +1,10 @@ use camino::Utf8Path; +use icp_fs::yaml::{LoadYamlFileError, load_yaml_file}; +use model::{CustomAdapter, MotokoAdapter, RustAdapter}; use serde::Deserialize; use snafu::Snafu; -use icp_fs::yaml::{LoadYamlFileError, load_yaml_file}; - mod model; -use model::{CustomAdapter, MotokoAdapter, RustAdapter}; /// Identifies the type of adapter used to build the canister, /// along with its configuration. diff --git a/lib/icp-project/src/directory.rs b/lib/icp-project/src/directory.rs index d7babd174..de4405f05 100644 --- a/lib/icp-project/src/directory.rs +++ b/lib/icp-project/src/directory.rs @@ -1,10 +1,8 @@ -use std::io; - -use camino::Utf8PathBuf; -use snafu::{ResultExt, Snafu}; - use crate::structure::ProjectDirectoryStructure; +use camino::Utf8PathBuf; use icp_network::NetworkDirectory; +use snafu::{ResultExt, Snafu}; +use std::io; pub struct ProjectDirectory { structure: ProjectDirectoryStructure, diff --git a/lib/icp-project/src/lib.rs b/lib/icp-project/src/lib.rs index f8353395b..a4e48339d 100644 --- a/lib/icp-project/src/lib.rs +++ b/lib/icp-project/src/lib.rs @@ -1,8 +1,7 @@ use camino::{Utf8Path, Utf8PathBuf}; +use icp_fs::yaml::{LoadYamlFileError, load_yaml_file}; use serde::Deserialize; use snafu::{OptionExt, ResultExt, Snafu}; - -use icp_fs::yaml::{LoadYamlFileError, load_yaml_file}; use structure::ProjectDirectoryStructure; pub mod directory; From f5d73d32225e723c47e3fc1747bc9184af2786ca Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Tue, 3 Jun 2025 10:31:54 -0400 Subject: [PATCH 24/33] use canister manifest path from directory structure --- bin/icp-cli/src/commands/build.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/bin/icp-cli/src/commands/build.rs b/bin/icp-cli/src/commands/build.rs index a482bb376..c712cf897 100644 --- a/bin/icp-cli/src/commands/build.rs +++ b/bin/icp-cli/src/commands/build.rs @@ -8,21 +8,19 @@ use snafu::Snafu; pub struct Cmd; pub async fn dispatch(_cmd: Cmd) -> Result<(), BuildCommandError> { - let path = ProjectDirectory::find()? - .ok_or(BuildCommandError::ProjectNotFound)? - .structure() - .project_yaml_path(); + // Project + let pd = ProjectDirectory::find()?.ok_or(BuildCommandError::ProjectNotFound)?; - let pm = ProjectManifest::from_file(&path)?; + let pds = pd.structure(); + + // Load + let pm = ProjectManifest::from_file(pds.project_yaml_path())?; // List canisters in project let mut cs = Vec::new(); for c in pm.canisters { - let path = c.join("canister.yaml"); - - let cm = CanisterManifest::from_file(&path)?; - + let cm = CanisterManifest::from_file(pds.canister_yaml_path(&c))?; cs.push(cm); } From bdd98b71ebfadffb086dbba51d53e6c4ecfdeb11 Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Tue, 3 Jun 2025 10:34:37 -0400 Subject: [PATCH 25/33] leaf subcommands should use an exec name and not dispatch --- bin/icp-cli/src/commands.rs | 2 +- bin/icp-cli/src/commands/build.rs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/icp-cli/src/commands.rs b/bin/icp-cli/src/commands.rs index 5f9e2040c..cec84bbb1 100644 --- a/bin/icp-cli/src/commands.rs +++ b/bin/icp-cli/src/commands.rs @@ -19,7 +19,7 @@ pub enum Subcmd { pub async fn dispatch(cli: Cli) -> Result<(), DispatchError> { match cli.subcommand { - Subcmd::Build(opts) => build::dispatch(opts).await?, + Subcmd::Build(opts) => build::exec(opts).await?, Subcmd::Network(opts) => network::dispatch(opts).await?, } Ok(()) diff --git a/bin/icp-cli/src/commands/build.rs b/bin/icp-cli/src/commands/build.rs index c712cf897..80a5031cb 100644 --- a/bin/icp-cli/src/commands/build.rs +++ b/bin/icp-cli/src/commands/build.rs @@ -7,10 +7,11 @@ use snafu::Snafu; #[derive(Parser, Debug)] pub struct Cmd; -pub async fn dispatch(_cmd: Cmd) -> Result<(), BuildCommandError> { +pub async fn exec(_cmd: Cmd) -> Result<(), BuildCommandError> { // Project let pd = ProjectDirectory::find()?.ok_or(BuildCommandError::ProjectNotFound)?; + // Project Structure (paths, etc) let pds = pd.structure(); // Load From 00afea2fa457bdaba058b8197c0501182bd827c5 Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Tue, 3 Jun 2025 10:51:52 -0400 Subject: [PATCH 26/33] move everything to model file --- bin/icp-cli/src/commands/build.rs | 2 +- lib/icp-canister/src/lib.rs | 66 +------------------------------ lib/icp-canister/src/model.rs | 61 ++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 66 deletions(-) diff --git a/bin/icp-cli/src/commands/build.rs b/bin/icp-cli/src/commands/build.rs index 80a5031cb..b4a7a932e 100644 --- a/bin/icp-cli/src/commands/build.rs +++ b/bin/icp-cli/src/commands/build.rs @@ -1,5 +1,5 @@ use clap::Parser; -use icp_canister::{CanisterManifest, LoadCanisterManifestError}; +use icp_canister::model::{CanisterManifest, LoadCanisterManifestError}; use icp_project::directory::{FindProjectError, ProjectDirectory}; use icp_project::{LoadProjectManifestError, ProjectManifest}; use snafu::Snafu; diff --git a/lib/icp-canister/src/lib.rs b/lib/icp-canister/src/lib.rs index 25844f7d0..65880be0e 100644 --- a/lib/icp-canister/src/lib.rs +++ b/lib/icp-canister/src/lib.rs @@ -1,65 +1 @@ -use camino::Utf8Path; -use icp_fs::yaml::{LoadYamlFileError, load_yaml_file}; -use model::{CustomAdapter, MotokoAdapter, RustAdapter}; -use serde::Deserialize; -use snafu::Snafu; - -mod model; - -/// Identifies the type of adapter used to build the canister, -/// along with its configuration. -/// -/// The adapter type is specified via the `type` field in the YAML file. -/// For example: -/// -/// ```yaml -/// type: rust -/// package: my_canister -/// ``` -#[derive(Debug, Deserialize)] -#[serde(tag = "type", rename_all = "lowercase")] -pub enum Adapter { - /// A canister written in Rust. - Rust(RustAdapter), - - /// A canister written in Motoko. - Motoko(MotokoAdapter), - - /// A canister built using a custom script or command. - Custom(CustomAdapter), -} - -/// Describes how the canister should be built into WebAssembly, -/// including the adapter responsible for the build. -#[derive(Debug, Deserialize)] -pub struct Build { - pub adapter: Adapter, -} - -/// Represents the manifest describing a single canister, -/// including its name and how it should be built. -#[derive(Debug, Deserialize)] -pub struct CanisterManifest { - /// Name of the canister described by this manifest. - pub name: String, - - /// Build configuration for producing the canister's WebAssembly. - pub build: Build, -} - -impl CanisterManifest { - pub fn from_file>(path: P) -> Result { - let path = path.as_ref(); - - // Load - let cm: CanisterManifest = load_yaml_file(path)?; - - Ok(cm) - } -} - -#[derive(Debug, Snafu)] -pub enum LoadCanisterManifestError { - #[snafu(transparent)] - Parse { source: LoadYamlFileError }, -} +pub mod model; diff --git a/lib/icp-canister/src/model.rs b/lib/icp-canister/src/model.rs index 65edac7cd..9ca69d3d4 100644 --- a/lib/icp-canister/src/model.rs +++ b/lib/icp-canister/src/model.rs @@ -1,4 +1,7 @@ +use camino::Utf8Path; +use icp_fs::yaml::{LoadYamlFileError, load_yaml_file}; use serde::Deserialize; +use snafu::Snafu; /// Configuration for a Rust-based canister build adapter. #[derive(Debug, Deserialize)] @@ -22,3 +25,61 @@ pub struct CustomAdapter { /// Path to a script or executable used to build the canister. pub script: String, } + +/// Identifies the type of adapter used to build the canister, +/// along with its configuration. +/// +/// The adapter type is specified via the `type` field in the YAML file. +/// For example: +/// +/// ```yaml +/// type: rust +/// package: my_canister +/// ``` +#[derive(Debug, Deserialize)] +#[serde(tag = "type", rename_all = "lowercase")] +pub enum Adapter { + /// A canister written in Rust. + Rust(RustAdapter), + + /// A canister written in Motoko. + Motoko(MotokoAdapter), + + /// A canister built using a custom script or command. + Custom(CustomAdapter), +} + +/// Describes how the canister should be built into WebAssembly, +/// including the adapter responsible for the build. +#[derive(Debug, Deserialize)] +pub struct Build { + pub adapter: Adapter, +} + +/// Represents the manifest describing a single canister, +/// including its name and how it should be built. +#[derive(Debug, Deserialize)] +pub struct CanisterManifest { + /// Name of the canister described by this manifest. + pub name: String, + + /// Build configuration for producing the canister's WebAssembly. + pub build: Build, +} + +impl CanisterManifest { + pub fn from_file>(path: P) -> Result { + let path = path.as_ref(); + + // Load + let cm: CanisterManifest = load_yaml_file(path)?; + + Ok(cm) + } +} + +#[derive(Debug, Snafu)] +pub enum LoadCanisterManifestError { + #[snafu(transparent)] + Parse { source: LoadYamlFileError }, +} From 4db89358ae1c323220f2396880eab8e79db857d1 Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Wed, 4 Jun 2025 09:42:58 -0400 Subject: [PATCH 27/33] move definitions to model file --- bin/icp-cli/src/commands/build.rs | 2 +- lib/icp-project/src/lib.rs | 99 +------------------------------ lib/icp-project/src/model.rs | 97 ++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 99 deletions(-) create mode 100644 lib/icp-project/src/model.rs diff --git a/bin/icp-cli/src/commands/build.rs b/bin/icp-cli/src/commands/build.rs index b4a7a932e..ce1078608 100644 --- a/bin/icp-cli/src/commands/build.rs +++ b/bin/icp-cli/src/commands/build.rs @@ -1,7 +1,7 @@ use clap::Parser; use icp_canister::model::{CanisterManifest, LoadCanisterManifestError}; use icp_project::directory::{FindProjectError, ProjectDirectory}; -use icp_project::{LoadProjectManifestError, ProjectManifest}; +use icp_project::model::{LoadProjectManifestError, ProjectManifest}; use snafu::Snafu; #[derive(Parser, Debug)] diff --git a/lib/icp-project/src/lib.rs b/lib/icp-project/src/lib.rs index a4e48339d..994d078dc 100644 --- a/lib/icp-project/src/lib.rs +++ b/lib/icp-project/src/lib.rs @@ -1,100 +1,3 @@ -use camino::{Utf8Path, Utf8PathBuf}; -use icp_fs::yaml::{LoadYamlFileError, load_yaml_file}; -use serde::Deserialize; -use snafu::{OptionExt, ResultExt, Snafu}; -use structure::ProjectDirectoryStructure; - pub mod directory; +pub mod model; pub mod structure; - -/// Provides the default glob pattern for locating canister manifests -/// when the `canisters` field is not explicitly specified in the YAML. -fn default_canisters() -> Vec { - ["canisters/*"].iter().map(Utf8PathBuf::from).collect() -} - -/// Provides the default glob pattern for locating network definition files -/// when the `networks` field is not explicitly specified in the YAML. -fn default_networks() -> Vec { - ["networks/*"].iter().map(Utf8PathBuf::from).collect() -} - -/// Represents the manifest for an ICP project, typically loaded from `icp.yaml`. -/// A project is a repository or directory grouping related canisters and network definitions. -#[derive(Debug, Deserialize)] -pub struct ProjectManifest { - /// List of canister manifests belonging to this project. - /// Supports glob patterns to specify multiple canister YAML files. - #[serde(default = "default_canisters")] - pub canisters: Vec, - - /// List of network definition files relevant to the project. - /// Supports glob patterns to reference multiple network config files. - #[serde(default = "default_networks")] - pub networks: Vec, -} - -impl ProjectManifest { - pub fn from_file>(path: P) -> Result { - let mpath = path.as_ref(); - - // Project - let pds = ProjectDirectoryStructure::new(mpath); - - // Load - let mut pm: ProjectManifest = load_yaml_file(mpath)?; - - // Project canisters - let mut cs = Vec::new(); - - for pattern in pm.canisters { - let mdir = mpath - .parent() - .context(ProjectDirectorySnafu { path: mpath })?; - - let matches = - glob::glob(mdir.join(&pattern).as_str()).context(GlobPatternSnafu { pattern })?; - - for cpath in matches { - let cpath = cpath.context(GlobWalkSnafu { path: mpath })?; - - let path: Utf8PathBuf = cpath.try_into()?; - - // Skip non-canister directories - if !pds.canister_yaml_path(&path).exists() { - continue; - } - - cs.push(path); - } - } - - pm.canisters = cs; - - Ok(pm) - } -} - -#[derive(Debug, Snafu)] -pub enum LoadProjectManifestError { - #[snafu(display("failed to find project directory for project manifest {path}"))] - ProjectDirectory { path: Utf8PathBuf }, - - #[snafu(transparent)] - Parse { source: LoadYamlFileError }, - - #[snafu(transparent)] - InvalidPathUtf8 { source: camino::FromPathBufError }, - - #[snafu(display("failed to glob pattern {pattern}"))] - GlobPattern { - source: glob::PatternError, - pattern: String, - }, - - #[snafu(display("failed to glob pattern in {path}"))] - GlobWalk { - source: glob::GlobError, - path: Utf8PathBuf, - }, -} diff --git a/lib/icp-project/src/model.rs b/lib/icp-project/src/model.rs new file mode 100644 index 000000000..1afb2daf9 --- /dev/null +++ b/lib/icp-project/src/model.rs @@ -0,0 +1,97 @@ +use crate::structure::ProjectDirectoryStructure; +use camino::{Utf8Path, Utf8PathBuf}; +use icp_fs::yaml::{LoadYamlFileError, load_yaml_file}; +use serde::Deserialize; +use snafu::{OptionExt, ResultExt, Snafu}; + +/// Provides the default glob pattern for locating canister manifests +/// when the `canisters` field is not explicitly specified in the YAML. +fn default_canisters() -> Vec { + ["canisters/*"].iter().map(Utf8PathBuf::from).collect() +} + +/// Provides the default glob pattern for locating network definition files +/// when the `networks` field is not explicitly specified in the YAML. +fn default_networks() -> Vec { + ["networks/*"].iter().map(Utf8PathBuf::from).collect() +} + +/// Represents the manifest for an ICP project, typically loaded from `icp.yaml`. +/// A project is a repository or directory grouping related canisters and network definitions. +#[derive(Debug, Deserialize)] +pub struct ProjectManifest { + /// List of canister manifests belonging to this project. + /// Supports glob patterns to specify multiple canister YAML files. + #[serde(default = "default_canisters")] + pub canisters: Vec, + + /// List of network definition files relevant to the project. + /// Supports glob patterns to reference multiple network config files. + #[serde(default = "default_networks")] + pub networks: Vec, +} + +impl ProjectManifest { + pub fn from_file>(path: P) -> Result { + let mpath = path.as_ref(); + + // Project + let pds = ProjectDirectoryStructure::new(mpath); + + // Load + let mut pm: ProjectManifest = load_yaml_file(mpath)?; + + // Project canisters + let mut cs = Vec::new(); + + for pattern in pm.canisters { + let mdir = mpath + .parent() + .context(ProjectDirectorySnafu { path: mpath })?; + + let matches = + glob::glob(mdir.join(&pattern).as_str()).context(GlobPatternSnafu { pattern })?; + + for cpath in matches { + let cpath = cpath.context(GlobWalkSnafu { path: mpath })?; + + let path: Utf8PathBuf = cpath.try_into()?; + + // Skip non-canister directories + if !pds.canister_yaml_path(&path).exists() { + continue; + } + + cs.push(path); + } + } + + pm.canisters = cs; + + Ok(pm) + } +} + +#[derive(Debug, Snafu)] +pub enum LoadProjectManifestError { + #[snafu(display("failed to find project directory for project manifest {path}"))] + ProjectDirectory { path: Utf8PathBuf }, + + #[snafu(transparent)] + Parse { source: LoadYamlFileError }, + + #[snafu(transparent)] + InvalidPathUtf8 { source: camino::FromPathBufError }, + + #[snafu(display("failed to glob pattern {pattern}"))] + GlobPattern { + source: glob::PatternError, + pattern: String, + }, + + #[snafu(display("failed to glob pattern in {path}"))] + GlobWalk { + source: glob::GlobError, + path: Utf8PathBuf, + }, +} From 00b365a726a4c9669222e7e711a3e24f40c9d8a0 Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Wed, 4 Jun 2025 09:48:55 -0400 Subject: [PATCH 28/33] use project-directory-structure to load project manifest --- bin/icp-cli/src/commands/build.rs | 2 +- lib/icp-project/src/model.rs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/bin/icp-cli/src/commands/build.rs b/bin/icp-cli/src/commands/build.rs index ce1078608..576bee751 100644 --- a/bin/icp-cli/src/commands/build.rs +++ b/bin/icp-cli/src/commands/build.rs @@ -15,7 +15,7 @@ pub async fn exec(_cmd: Cmd) -> Result<(), BuildCommandError> { let pds = pd.structure(); // Load - let pm = ProjectManifest::from_file(pds.project_yaml_path())?; + let pm = ProjectManifest::try_from(pds)?; // List canisters in project let mut cs = Vec::new(); diff --git a/lib/icp-project/src/model.rs b/lib/icp-project/src/model.rs index 1afb2daf9..8eb185b91 100644 --- a/lib/icp-project/src/model.rs +++ b/lib/icp-project/src/model.rs @@ -31,17 +31,17 @@ pub struct ProjectManifest { pub networks: Vec, } -impl ProjectManifest { - pub fn from_file>(path: P) -> Result { - let mpath = path.as_ref(); +impl TryFrom<&ProjectDirectoryStructure> for ProjectManifest { + type Error = LoadProjectManifestError; - // Project - let pds = ProjectDirectoryStructure::new(mpath); + fn try_from(pds: &ProjectDirectoryStructure) -> Result { + let mpath = pds.project_yaml_path(); + let mpath: &Utf8Path = mpath.as_ref(); // Load let mut pm: ProjectManifest = load_yaml_file(mpath)?; - // Project canisters + // Canisters let mut cs = Vec::new(); for pattern in pm.canisters { From 3ef371f1e8545e3b30aaba055bb5c44db866bf1d Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Wed, 4 Jun 2025 09:58:23 -0400 Subject: [PATCH 29/33] use pds when available --- lib/icp-project/src/model.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/icp-project/src/model.rs b/lib/icp-project/src/model.rs index 8eb185b91..7eb0fd1b5 100644 --- a/lib/icp-project/src/model.rs +++ b/lib/icp-project/src/model.rs @@ -45,12 +45,8 @@ impl TryFrom<&ProjectDirectoryStructure> for ProjectManifest { let mut cs = Vec::new(); for pattern in pm.canisters { - let mdir = mpath - .parent() - .context(ProjectDirectorySnafu { path: mpath })?; - - let matches = - glob::glob(mdir.join(&pattern).as_str()).context(GlobPatternSnafu { pattern })?; + let matches = glob::glob(pds.root().join(&pattern).as_str()) + .context(GlobPatternSnafu { pattern })?; for cpath in matches { let cpath = cpath.context(GlobWalkSnafu { path: mpath })?; From b702c48e903b214853584c2c7e6415d0dae62eb0 Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Wed, 4 Jun 2025 09:59:48 -0400 Subject: [PATCH 30/33] rename conversion function to load --- bin/icp-cli/src/commands/build.rs | 2 +- lib/icp-project/src/model.rs | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/bin/icp-cli/src/commands/build.rs b/bin/icp-cli/src/commands/build.rs index 576bee751..2eb42f09a 100644 --- a/bin/icp-cli/src/commands/build.rs +++ b/bin/icp-cli/src/commands/build.rs @@ -15,7 +15,7 @@ pub async fn exec(_cmd: Cmd) -> Result<(), BuildCommandError> { let pds = pd.structure(); // Load - let pm = ProjectManifest::try_from(pds)?; + let pm = ProjectManifest::load(pds)?; // List canisters in project let mut cs = Vec::new(); diff --git a/lib/icp-project/src/model.rs b/lib/icp-project/src/model.rs index 7eb0fd1b5..e4040f19f 100644 --- a/lib/icp-project/src/model.rs +++ b/lib/icp-project/src/model.rs @@ -31,10 +31,8 @@ pub struct ProjectManifest { pub networks: Vec, } -impl TryFrom<&ProjectDirectoryStructure> for ProjectManifest { - type Error = LoadProjectManifestError; - - fn try_from(pds: &ProjectDirectoryStructure) -> Result { +impl ProjectManifest { + pub fn load(pds: &ProjectDirectoryStructure) -> Result { let mpath = pds.project_yaml_path(); let mpath: &Utf8Path = mpath.as_ref(); From ee4fb21d2b446d82e5922ac48914f754d2b280f6 Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Wed, 4 Jun 2025 10:06:26 -0400 Subject: [PATCH 31/33] remove unused error variant --- lib/icp-project/src/model.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/icp-project/src/model.rs b/lib/icp-project/src/model.rs index e4040f19f..8260fdebe 100644 --- a/lib/icp-project/src/model.rs +++ b/lib/icp-project/src/model.rs @@ -68,9 +68,6 @@ impl ProjectManifest { #[derive(Debug, Snafu)] pub enum LoadProjectManifestError { - #[snafu(display("failed to find project directory for project manifest {path}"))] - ProjectDirectory { path: Utf8PathBuf }, - #[snafu(transparent)] Parse { source: LoadYamlFileError }, From b6a2dbd7db0974f91a46c88f0e305a50dc1d4838 Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Wed, 4 Jun 2025 10:16:02 -0400 Subject: [PATCH 32/33] add documentation about load function behavior --- lib/icp-project/src/model.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/lib/icp-project/src/model.rs b/lib/icp-project/src/model.rs index 8260fdebe..20c3e6803 100644 --- a/lib/icp-project/src/model.rs +++ b/lib/icp-project/src/model.rs @@ -32,6 +32,30 @@ pub struct ProjectManifest { } impl ProjectManifest { + /// Loads the project manifest (`project.yaml`) and resolves canister paths. + /// + /// This function utilizes the provided [`ProjectDirectoryStructure`] to locate + /// the `project.yaml` file and then identify all canister directories + /// referenced within it. + /// + /// # Canister Path Resolution + /// + /// Currently, all paths specified in the `canisters` field of the manifest + /// are treated as glob patterns. This means that even if a direct path to a + /// canister directory is provided (e.g., `canisters/my_canister`), it will + /// be processed as a glob. + /// + /// A consequence of this glob-based approach is that if an explicitly + /// specified canister path does not contain a `canister.yaml` file (thus, + /// not being a valid canister directory according to `ProjectDirectoryStructure`), + /// it will be silently ignored rather than causing an error. + /// + /// **Future Improvement:** This behavior should be changed. In a future + /// version, if a path in the `canisters` list is *not* a glob pattern (i.e., + /// it's an explicit path), and that path does not point to a valid canister + /// directory (i.e., it's missing a `canister.yaml` or is not a directory), + /// the loading process should raise an error. This will provide clearer + /// feedback for misconfigured manifests. pub fn load(pds: &ProjectDirectoryStructure) -> Result { let mpath = pds.project_yaml_path(); let mpath: &Utf8Path = mpath.as_ref(); From 21ea7c561e27e79c56e439686094b20c7f6d4d6b Mon Sep 17 00:00:00 2001 From: Or Ricon Date: Wed, 4 Jun 2025 10:16:54 -0400 Subject: [PATCH 33/33] fix lint error --- lib/icp-project/src/model.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/icp-project/src/model.rs b/lib/icp-project/src/model.rs index 20c3e6803..5d92ee718 100644 --- a/lib/icp-project/src/model.rs +++ b/lib/icp-project/src/model.rs @@ -2,7 +2,7 @@ use crate::structure::ProjectDirectoryStructure; use camino::{Utf8Path, Utf8PathBuf}; use icp_fs::yaml::{LoadYamlFileError, load_yaml_file}; use serde::Deserialize; -use snafu::{OptionExt, ResultExt, Snafu}; +use snafu::{ResultExt, Snafu}; /// Provides the default glob pattern for locating canister manifests /// when the `canisters` field is not explicitly specified in the YAML.