diff --git a/cargo-near/src/commands/build_command/build.rs b/cargo-near/src/commands/build_command/build.rs index 8aca024a..139c2df3 100644 --- a/cargo-near/src/commands/build_command/build.rs +++ b/cargo-near/src/commands/build_command/build.rs @@ -12,7 +12,9 @@ use crate::util; const COMPILATION_TARGET: &str = "wasm32-unknown-unknown"; -pub fn run(args: super::BuildCommand) -> color_eyre::eyre::Result { +pub(super) fn run( + args: super::BuildCommand, +) -> color_eyre::eyre::Result { let color = args.color.unwrap_or(ColorPreference::Auto); color.apply(); diff --git a/cargo-near/src/commands/build_command/docker.rs b/cargo-near/src/commands/build_command/docker.rs new file mode 100644 index 00000000..810a8e65 --- /dev/null +++ b/cargo-near/src/commands/build_command/docker.rs @@ -0,0 +1,222 @@ +use std::{ + process::{id, Command, ExitStatus}, + time::{SystemTime, UNIX_EPOCH}, +}; + +use crate::types::manifest::CargoManifestPath; +use crate::{types::metadata::CrateMetadata, util}; + +use color_eyre::{ + eyre::{ContextCompat, WrapErr}, + owo_colors::OwoColorize, +}; + +#[cfg(unix)] +use nix::unistd::{getgid, getuid}; + +use super::BuildContext; + +mod cloned_repo; +mod git_checks; +mod metadata; + +impl super::BuildCommand { + pub(super) fn docker_run( + self, + context: BuildContext, + ) -> color_eyre::eyre::Result { + util::handle_step("Performing git checks...", || { + match context { + BuildContext::Deploy => { + let contract_path: camino::Utf8PathBuf = self.contract_path()?; + // TODO: clone to tmp folder and checkout specific revision must be separate steps + eprintln!( + "\n The URL of the remote repository:\n {}\n", + git_checks::remote_repo_url(&contract_path)? + ); + Ok(()) + } + BuildContext::Build => Ok(()), + } + })?; + let mut cloned_repo = util::handle_step( + "Cloning project repo to a temporary build site, removing uncommitted changes...", + || cloned_repo::ClonedRepo::clone(&self), + )?; + + let crate_metadata = util::handle_step( + "Collecting cargo project metadata from temporary build site...", + || { + let cargo_toml_path: camino::Utf8PathBuf = { + let mut cloned_path: std::path::PathBuf = cloned_repo.tmp_contract_path.clone(); + cloned_path.push("Cargo.toml"); + cloned_path.try_into()? + }; + CrateMetadata::collect(CargoManifestPath::try_from(cargo_toml_path)?) + }, + )?; + + let docker_build_meta = + util::handle_step("Parsing and validating `Cargo.toml` metadata...", || { + metadata::ReproducibleBuild::parse(&crate_metadata) + })?; + + util::print_step("Running docker command step..."); + let (status, docker_cmd) = self.docker_subprocess_step(docker_build_meta, &cloned_repo)?; + + if status.success() { + util::print_success("Running docker command step (finished)"); + // TODO: make this a `ClonedRepo` `copy_artifact` method + cloned_repo.tmp_contract_path.push("target"); + cloned_repo.tmp_contract_path.push("near"); + + let dir = cloned_repo.tmp_contract_path.read_dir().wrap_err_with(|| { + format!( + "No artifacts directory found: `{:?}`.", + cloned_repo.tmp_contract_path + ) + })?; + + for entry in dir.flatten() { + if entry.path().extension().unwrap().to_str().unwrap() == "wasm" { + let wasm_path = { + let mut contract_path = cloned_repo.contract_path.clone(); + contract_path.push("contract.wasm"); + contract_path + }; + std::fs::copy::( + entry.path(), + wasm_path.clone(), + )?; + + // TODO: ensure fresh + return Ok(util::CompilationArtifact { + path: wasm_path, + fresh: true, + from_docker: true, + }); + } + } + + Err(color_eyre::eyre::eyre!( + "Wasm file not found in directory: `{:?}`.", + cloned_repo.tmp_contract_path + )) + } else { + println!(); + println!( + "{}", + format!( + "See output above ↑↑↑.\nSourceScan command `{:?}` failed with exit status: {status}.", + docker_cmd + ).yellow() + ); + + Err(color_eyre::eyre::eyre!( + "Reproducible build in docker container failed" + )) + } + } + + fn docker_subprocess_step( + self, + docker_build_meta: metadata::ReproducibleBuild, + cloned_repo: &cloned_repo::ClonedRepo, + ) -> color_eyre::eyre::Result<(ExitStatus, Command)> { + let mut cargo_args = vec![]; + + if self.no_abi { + cargo_args.push("--no-abi") + } + if self.no_embed_abi { + cargo_args.push("--no-embed-abi") + } + if self.no_doc { + cargo_args.push("--no-doc") + } + + let color = self + .color + .clone() + .unwrap_or(crate::common::ColorPreference::Auto) + .to_string(); + cargo_args.extend(&["--color", &color]); + // Cross-platform process ID and timestamp + let pid = id().to_string(); + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + .to_string(); + + let volume = format!( + "{}:/host", + cloned_repo + .tmp_repo + .workdir() + .wrap_err("Could not get the working directory for the repository")? + .to_string_lossy() + ); + let docker_container_name = format!("cargo-near-{}-{}", timestamp, pid); + let docker_image = docker_build_meta.concat_image(); + let near_build_env_ref = format!("NEAR_BUILD_ENVIRONMENT_REF={}", docker_image); + + // Platform-specific UID/GID retrieval + #[cfg(unix)] + let uid_gid = format!("{}:{}", getuid(), getgid()); + #[cfg(not(unix))] + let uid_gid = "1000:1000".to_string(); + + let mut docker_args = vec![ + "-u", + &uid_gid, + "-it", + "--name", + &docker_container_name, + "--volume", + &volume, + "--rm", + "--workdir", + "/host", + "--env", + &near_build_env_ref, + "--env", + "RUST_LOG=cargo_near=info", + &docker_image, + "/bin/bash", + "-c", + ]; + + let mut cargo_cmd_list = vec!["cargo", "near", "build"]; + cargo_cmd_list.extend(&cargo_args); + + let cargo_cmd = cargo_cmd_list.join(" "); + + docker_args.push(&cargo_cmd); + log::debug!("docker command : {:?}", docker_args); + + let mut docker_cmd = Command::new("docker"); + docker_cmd.arg("run"); + docker_cmd.args(docker_args); + + let status = match docker_cmd.status() { + Ok(exit_status) => exit_status, + Err(io_err) => { + println!(); + println!( + "{}", + format!( + "Error obtaining status from executing SourceScan command `{:?}`", + docker_cmd + ) + .yellow() + ); + println!("{}", format!("Error `{:?}`", io_err).yellow()); + return Err(color_eyre::eyre::eyre!( + "Reproducible build in docker container failed" + )); + } + }; + Ok((status, docker_cmd)) + } +} diff --git a/cargo-near/src/commands/build_command/docker/cloned_repo.rs b/cargo-near/src/commands/build_command/docker/cloned_repo.rs new file mode 100644 index 00000000..a74b6996 --- /dev/null +++ b/cargo-near/src/commands/build_command/docker/cloned_repo.rs @@ -0,0 +1,27 @@ +use crate::commands::build_command::BuildCommand; + +pub(super) struct ClonedRepo { + pub tmp_repo: git2::Repository, + pub tmp_contract_path: std::path::PathBuf, + pub contract_path: camino::Utf8PathBuf, + #[allow(unused)] + tmp_contract_dir: tempfile::TempDir, +} + +impl ClonedRepo { + pub(super) fn clone(args: &BuildCommand) -> color_eyre::eyre::Result { + let contract_path: camino::Utf8PathBuf = args.contract_path()?; + log::info!("ClonedRepo.contract_path: {:?}", contract_path,); + + let tmp_contract_dir = tempfile::tempdir()?; + let tmp_contract_path = tmp_contract_dir.path().to_path_buf(); + log::info!("ClonedRepo.tmp_contract_path: {:?}", tmp_contract_path); + let tmp_repo = git2::Repository::clone(contract_path.as_str(), &tmp_contract_path)?; + Ok(ClonedRepo { + tmp_repo, + tmp_contract_path, + tmp_contract_dir, + contract_path, + }) + } +} diff --git a/cargo-near/src/commands/build_command/docker/git_checks.rs b/cargo-near/src/commands/build_command/docker/git_checks.rs new file mode 100644 index 00000000..90ef325b --- /dev/null +++ b/cargo-near/src/commands/build_command/docker/git_checks.rs @@ -0,0 +1,139 @@ +use color_eyre::eyre::{ContextCompat, WrapErr}; +use serde_json::to_string; + +pub(super) fn remote_repo_url( + contract_path: &camino::Utf8PathBuf, +) -> color_eyre::Result { + let mut path_cargo_toml = contract_path.clone(); + path_cargo_toml.push("Cargo.toml"); + let cargo_toml = cargo_toml::Manifest::from_slice( + &std::fs::read(&path_cargo_toml) + .wrap_err_with(|| format!("Failed to read file <{path_cargo_toml}>"))?, + ) + .wrap_err("Could not parse 'Cargo.toml'")?; + + let mut remote_repo_url = reqwest::Url::parse( + cargo_toml + .package() + .repository() + .wrap_err("No reference to the remote repository for this contract was found in the file 'Cargo.toml'.\ + \nAdd the value 'repository' to the '[package]' section to continue deployment.")? + )?; + + let path = remote_repo_url.path().trim_end_matches('/'); + + let repo_id = check_repo_state(contract_path)?.to_string(); + + let commit = format!("{path}/commit/{repo_id}"); + + remote_repo_url.set_path(&commit); + log::info!("checking existence of {}", remote_repo_url); + + let mut retries_left = (0..5).rev(); + loop { + let response = reqwest::blocking::get(remote_repo_url.clone())?; + + if retries_left.next().is_none() { + color_eyre::eyre::bail!("Currently, it is not possible to check for remote repository <{remote_repo_url}>. Try again after a while.") + } + + // Check if status is within 100-199. + if response.status().is_informational() { + eprintln!("Transport error.\nPlease wait. The next try to send this query is happening right now ..."); + } + + // Check if status is within 200-299. + if response.status().is_success() { + return Ok(remote_repo_url); + } + + // Check if status is within 300-399. + if response.status().is_redirection() { + return Ok(remote_repo_url); + } + + // Check if status is within 400-499. + if response.status().is_client_error() { + color_eyre::eyre::bail!("Remote repository <{remote_repo_url}> does not exist.") + } + + // Check if status is within 500-599. + if response.status().is_server_error() { + eprintln!("Transport error.\nPlease wait. The next try to send this query is happening right now ..."); + } + + std::thread::sleep(std::time::Duration::from_millis(100)); + } +} + +fn check_repo_state(contract_path: &camino::Utf8PathBuf) -> color_eyre::Result { + let repo = git2::Repository::open(contract_path)?; + let mut dirty_files = Vec::new(); + collect_statuses(&repo, &mut dirty_files)?; + // Include each submodule so that the error message can provide + // specifically *which* files in a submodule are modified. + status_submodules(&repo, &mut dirty_files)?; + + if dirty_files.is_empty() { + return Ok(repo.revparse_single("HEAD")?.id()); + } + color_eyre::eyre::bail!( + "{} files in the working directory contain changes that were \ + not yet committed into git:\n\n{}\n\n\ + commit these changes to continue deployment", + dirty_files.len(), + dirty_files + .iter() + .map(to_string) + .collect::, _>>() + .wrap_err("Error parsing PathBaf")? + .join("\n") + ) +} + +// Helper to collect dirty statuses for a single repo. +fn collect_statuses( + repo: &git2::Repository, + dirty_files: &mut Vec, +) -> near_cli_rs::CliResult { + let mut status_opts = git2::StatusOptions::new(); + // Exclude submodules, as they are being handled manually by recursing + // into each one so that details about specific files can be + // retrieved. + status_opts + .exclude_submodules(true) + .include_ignored(true) + .include_untracked(true); + let repo_statuses = repo.statuses(Some(&mut status_opts)).with_context(|| { + format!( + "Failed to retrieve git status from repo {}", + repo.path().display() + ) + })?; + let workdir = repo.workdir().unwrap(); + let this_dirty = repo_statuses.iter().filter_map(|entry| { + let path = entry.path().expect("valid utf-8 path"); + if path.ends_with("Cargo.lock") || entry.status() == git2::Status::IGNORED { + return None; + } + Some(workdir.join(path)) + }); + dirty_files.extend(this_dirty); + Ok(()) +} + +// Helper to collect dirty statuses while recursing into submodules. +fn status_submodules( + repo: &git2::Repository, + dirty_files: &mut Vec, +) -> near_cli_rs::CliResult { + for submodule in repo.submodules()? { + // Ignore submodules that don't open, they are probably not initialized. + // If its files are required, then the verification step should fail. + if let Ok(sub_repo) = submodule.open() { + status_submodules(&sub_repo, dirty_files)?; + collect_statuses(&sub_repo, dirty_files)?; + } + } + Ok(()) +} diff --git a/cargo-near/src/commands/build_command/docker/metadata.rs b/cargo-near/src/commands/build_command/docker/metadata.rs new file mode 100644 index 00000000..69a855a9 --- /dev/null +++ b/cargo-near/src/commands/build_command/docker/metadata.rs @@ -0,0 +1,58 @@ +use color_eyre::owo_colors::OwoColorize; +use serde::Deserialize; + +use crate::{types::metadata::CrateMetadata, util}; + +#[derive(Deserialize, Debug)] +pub(super) struct ReproducibleBuild { + image: String, + image_digest: String, +} +impl ReproducibleBuild { + pub(super) fn parse(cargo_metadata: &CrateMetadata) -> color_eyre::eyre::Result { + let build_meta_value = cargo_metadata + .root_package + .metadata + .get("near") + .and_then(|value| value.get("reproducible_build")); + + let build_meta: ReproducibleBuild = match build_meta_value { + None => { + return Err(color_eyre::eyre::eyre!( + "Missing `[package.metadata.near.reproducible_build]` in Cargo.toml" + )) + } + Some(build_meta_value) => { + serde_json::from_value(build_meta_value.clone()).map_err(|err| { + color_eyre::eyre::eyre!( + "Malformed `[package.metadata.near.reproducible_build]` in Cargo.toml: {}", + err + ) + })? + } + }; + + println!( + "{}", + util::indent_string(&format!("reproducible build metadata: {:#?}", build_meta)).green() + ); + Ok(build_meta) + } + pub(super) fn concat_image(&self) -> String { + let mut result = String::new(); + result.push_str(&self.image); + result.push('@'); + result.push_str(&self.image_digest); + let result = result + .chars() + .filter(|c| c.is_ascii()) + .filter(|c| !c.is_ascii_control()) + .filter(|c| !c.is_ascii_whitespace()) + .collect(); + println!( + "{}", + format!(" docker image to be used: {}", result).green() + ); + result + } +} diff --git a/cargo-near/src/commands/build_command/mod.rs b/cargo-near/src/commands/build_command/mod.rs index a526c2c0..02938962 100644 --- a/cargo-near/src/commands/build_command/mod.rs +++ b/cargo-near/src/commands/build_command/mod.rs @@ -1,30 +1,15 @@ use std::ops::Deref; -use std::process::{id, Command, ExitStatus}; -use std::time::{SystemTime, UNIX_EPOCH}; -#[cfg(unix)] -use nix::unistd::{getgid, getuid}; +use crate::{types::manifest::CargoManifestPath, util}; -use color_eyre::{ - eyre::{ContextCompat, WrapErr}, - owo_colors::OwoColorize, -}; -use serde::Deserialize; - -use crate::{ - types::{manifest::CargoManifestPath, metadata::CrateMetadata}, - util, -}; - -pub mod build; +mod build; +mod docker; +pub const INSIDE_DOCKER_ENV_KEY: &str = "NEAR_BUILD_ENVIRONMENT_REF"; #[derive(Debug, Default, Clone, interactive_clap::InteractiveClap)] #[interactive_clap(input_context = near_cli_rs::GlobalContext)] #[interactive_clap(output_context = BuildCommandlContext)] pub struct BuildCommand { - /// Build contract without SourceScan verification - #[interactive_clap(long)] - pub no_docker: bool, /// Build contract in debug mode, without optimizations and bigger is size #[interactive_clap(long)] pub no_release: bool, @@ -52,6 +37,35 @@ pub struct BuildCommand { pub color: Option, } +#[derive(Debug)] +pub enum BuildContext { + Build, + Deploy, +} +impl BuildCommand { + pub fn contract_path(&self) -> color_eyre::eyre::Result { + let contract_path: camino::Utf8PathBuf = if let Some(manifest_path) = &self.manifest_path { + let manifest_path = CargoManifestPath::try_from(manifest_path.deref().clone())?; + manifest_path.directory()?.to_path_buf() + } else { + camino::Utf8PathBuf::from_path_buf(std::env::current_dir()?).map_err(|err| { + color_eyre::eyre::eyre!("Failed to convert path {}", err.to_string_lossy()) + })? + }; + Ok(contract_path) + } + pub fn run(self, context: BuildContext) -> color_eyre::eyre::Result { + if Self::no_docker() { + self::build::run(self) + } else { + self.docker_run(context) + } + } + pub fn no_docker() -> bool { + std::env::var(INSIDE_DOCKER_ENV_KEY).is_ok() + } +} + #[derive(Debug, Clone)] pub struct BuildCommandlContext; @@ -61,7 +75,6 @@ impl BuildCommandlContext { scope: &::InteractiveClapContextScope, ) -> color_eyre::eyre::Result { let args = BuildCommand { - no_docker: scope.no_docker, no_release: scope.no_release, no_abi: scope.no_abi, no_embed_abi: scope.no_embed_abi, @@ -70,279 +83,7 @@ impl BuildCommandlContext { manifest_path: scope.manifest_path.clone(), color: scope.color.clone(), }; - if args.no_docker { - self::build::run(args)?; - } else { - docker_run(args)?; - } + args.run(BuildContext::Build)?; Ok(Self) } } - -pub struct ClonedRepo { - pub tmp_repo: git2::Repository, - pub tmp_contract_path: std::path::PathBuf, - pub contract_path: camino::Utf8PathBuf, - #[allow(unused)] - tmp_contract_dir: tempfile::TempDir, -} - -fn clone_repo(args: &BuildCommand) -> color_eyre::eyre::Result { - let contract_path: camino::Utf8PathBuf = if let Some(manifest_path) = &args.manifest_path { - let manifest_path = CargoManifestPath::try_from(manifest_path.deref().clone())?; - manifest_path.directory()?.to_path_buf() - } else { - camino::Utf8PathBuf::from_path_buf(std::env::current_dir()?).map_err(|err| { - color_eyre::eyre::eyre!("Failed to convert path {}", err.to_string_lossy()) - })? - }; - log::info!("ClonedRepo.contract_path: {:?}", contract_path,); - - let tmp_contract_dir = tempfile::tempdir()?; - let tmp_contract_path = tmp_contract_dir.path().to_path_buf(); - log::info!("ClonedRepo.tmp_contract_path: {:?}", tmp_contract_path); - let tmp_repo = git2::Repository::clone(contract_path.as_str(), &tmp_contract_path)?; - Ok(ClonedRepo { - tmp_repo, - tmp_contract_path, - tmp_contract_dir, - contract_path, - }) -} - -#[derive(Deserialize, Debug)] -struct ReproducibleBuildMeta { - image: String, - image_digest: String, -} -impl ReproducibleBuildMeta { - pub fn concat_image(&self) -> String { - let mut result = String::new(); - result.push_str(&self.image); - result.push('@'); - result.push_str(&self.image_digest); - let result = result - .chars() - .filter(|c| c.is_ascii()) - .filter(|c| !c.is_ascii_control()) - .filter(|c| !c.is_ascii_whitespace()) - .collect(); - println!( - "{}", - format!(" docker image to be used: {}", result).green() - ); - result - } -} - -fn get_docker_build_meta( - cargo_metadata: &CrateMetadata, -) -> color_eyre::eyre::Result { - let build_meta_value = cargo_metadata - .root_package - .metadata - .get("near") - .and_then(|value| value.get("reproducible_build")); - - let build_meta: ReproducibleBuildMeta = match build_meta_value { - None => { - return Err(color_eyre::eyre::eyre!( - "Missing `[package.metadata.near.reproducible_build]` in Cargo.toml" - )) - } - Some(build_meta_value) => { - serde_json::from_value(build_meta_value.clone()).map_err(|err| { - color_eyre::eyre::eyre!( - "Malformed `[package.metadata.near.reproducible_build]` in Cargo.toml: {}", - err - ) - })? - } - }; - - println!( - "{}", - util::indent_string(&format!("reproducible build metadata: {:#?}", build_meta)).green() - ); - Ok(build_meta) -} - -fn docker_subprocess_step( - args: BuildCommand, - docker_build_meta: ReproducibleBuildMeta, - cloned_repo: &ClonedRepo, -) -> color_eyre::eyre::Result<(ExitStatus, Command)> { - let mut cargo_args = vec![]; - // Use this in new release version: - // let mut cargo_args = vec!["--no-docker"]; - - if args.no_abi { - cargo_args.push("--no-abi") - } - if args.no_embed_abi { - cargo_args.push("--no-embed-abi") - } - if args.no_doc { - cargo_args.push("--no-doc") - } - cargo_args.push("--no-docker"); - - let color = args - .color - .clone() - .unwrap_or(crate::common::ColorPreference::Auto) - .to_string(); - cargo_args.extend(&["--color", &color]); - // Cross-platform process ID and timestamp - let pid = id().to_string(); - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() - .to_string(); - - let volume = format!( - "{}:/host", - cloned_repo - .tmp_repo - .workdir() - .wrap_err("Could not get the working directory for the repository")? - .to_string_lossy() - ); - let docker_container_name = format!("cargo-near-{}-{}", timestamp, pid); - let docker_image = docker_build_meta.concat_image(); - let near_build_env_ref = format!("NEAR_BUILD_ENVIRONMENT_REF={}", docker_image); - - // Platform-specific UID/GID retrieval - #[cfg(unix)] - let uid_gid = format!("{}:{}", getuid(), getgid()); - #[cfg(not(unix))] - let uid_gid = "1000:1000".to_string(); - - let mut docker_args = vec![ - "-u", - &uid_gid, - "-it", - "--name", - &docker_container_name, - "--volume", - &volume, - "--rm", - "--workdir", - "/host", - "--env", - &near_build_env_ref, - "--env", - "RUST_LOG=cargo_near=info", - &docker_image, - "/bin/bash", - "-c", - ]; - - let mut cargo_cmd_list = vec!["cargo", "near", "build"]; - cargo_cmd_list.extend(&cargo_args); - - let cargo_cmd = cargo_cmd_list.join(" "); - - docker_args.push(&cargo_cmd); - log::debug!("docker command : {:?}", docker_args); - - let mut docker_cmd = Command::new("docker"); - docker_cmd.arg("run"); - docker_cmd.args(docker_args); - - let status = match docker_cmd.status() { - Ok(exit_status) => exit_status, - Err(io_err) => { - println!(); - println!( - "{}", - format!( - "Error obtaining status from executing SourceScan command `{:?}`", - docker_cmd - ) - .yellow() - ); - println!("{}", format!("Error `{:?}`", io_err).yellow()); - return Err(color_eyre::eyre::eyre!( - "Reproducible build in docker container failed" - )); - } - }; - Ok((status, docker_cmd)) -} - -pub fn docker_run(args: BuildCommand) -> color_eyre::eyre::Result { - let mut cloned_repo = util::handle_step( - "Cloning project repo to a temporary build site, removing uncommitted changes...", - || clone_repo(&args), - )?; - - let crate_metadata = util::handle_step( - "Collecting cargo project metadata from temporary build site...", - || { - let cargo_toml_path: camino::Utf8PathBuf = { - let mut cloned_path: std::path::PathBuf = cloned_repo.tmp_contract_path.clone(); - cloned_path.push("Cargo.toml"); - cloned_path.try_into()? - }; - CrateMetadata::collect(CargoManifestPath::try_from(cargo_toml_path)?) - }, - )?; - - let docker_build_meta = - util::handle_step("Parsing and validating `Cargo.toml` metadata...", || { - get_docker_build_meta(&crate_metadata) - })?; - - util::print_step("Running docker command step..."); - let (status, docker_cmd) = docker_subprocess_step(args, docker_build_meta, &cloned_repo)?; - - if status.success() { - util::print_success("Running docker command step (finished)"); - // TODO: make this a `ClonedRepo` `copy_artifact` method - cloned_repo.tmp_contract_path.push("target"); - cloned_repo.tmp_contract_path.push("near"); - - let dir = cloned_repo.tmp_contract_path.read_dir().wrap_err_with(|| { - format!( - "No artifacts directory found: `{:?}`.", - cloned_repo.tmp_contract_path - ) - })?; - - for entry in dir.flatten() { - if entry.path().extension().unwrap().to_str().unwrap() == "wasm" { - let wasm_path = { - let mut contract_path = cloned_repo.contract_path.clone(); - contract_path.push("contract.wasm"); - contract_path - }; - std::fs::copy::( - entry.path(), - wasm_path.clone(), - )?; - - return Ok(wasm_path); - } - } - - Err(color_eyre::eyre::eyre!( - "Wasm file not found in directory: `{:?}`.", - cloned_repo.tmp_contract_path - )) - } else { - println!(); - println!( - "{}", - format!( - "See output above ↑↑↑.\nSourceScan command `{:?}` failed with exit status: {status}.", - docker_cmd - ).yellow() - ); - - Err(color_eyre::eyre::eyre!( - "Reproducible build in docker container failed" - )) - } -} diff --git a/cargo-near/src/commands/deploy/mod.rs b/cargo-near/src/commands/deploy/mod.rs index 70879dd7..c601523e 100644 --- a/cargo-near/src/commands/deploy/mod.rs +++ b/cargo-near/src/commands/deploy/mod.rs @@ -1,6 +1,3 @@ -use color_eyre::eyre::{ContextCompat, WrapErr}; -use serde_json::to_string; - use near_cli_rs::commands::contract::deploy::initialize_mode::InitializeMode; use crate::commands::build_command; @@ -29,25 +26,11 @@ impl ContractContext { previous_context: near_cli_rs::GlobalContext, scope: &::InteractiveClapContextScope, ) -> color_eyre::eyre::Result { - let contract_path: camino::Utf8PathBuf = - if let Some(manifest_path) = &scope.build_command_args.manifest_path { - manifest_path.into() - } else { - camino::Utf8PathBuf::from_path_buf(std::env::current_dir()?).map_err(|err| { - color_eyre::eyre::eyre!("Failed to convert path {}", err.to_string_lossy()) - })? - }; - - let file_path = if !scope.build_command_args.no_docker { - // TODO: clone to tmp folder and checkout specific revision must be separate steps - eprintln!( - "\n The URL of the remote repository:\n {}\n", - remote_repo_url(&contract_path)? - ); - build_command::docker_run(scope.build_command_args.clone())? - } else { - build_command::build::run(scope.build_command_args.clone())?.path - }; + let file_path = scope + .build_command_args + .clone() + .run(build_command::BuildContext::Deploy)? + .path; Ok(Self( near_cli_rs::commands::contract::deploy::ContractFileContext { @@ -84,7 +67,6 @@ impl interactive_clap::FromCli for Contract { let build_command_args = if let Some(cli_build_command_args) = &clap_variant.build_command_args { build_command::BuildCommand { - no_docker: cli_build_command_args.no_docker, no_release: cli_build_command_args.no_release, no_abi: cli_build_command_args.no_abi, no_embed_abi: cli_build_command_args.no_embed_abi, @@ -148,138 +130,3 @@ impl Contract { ) } } - -fn check_repo_state(contract_path: &camino::Utf8PathBuf) -> color_eyre::Result { - let repo = git2::Repository::open(contract_path)?; - let mut dirty_files = Vec::new(); - collect_statuses(&repo, &mut dirty_files)?; - // Include each submodule so that the error message can provide - // specifically *which* files in a submodule are modified. - status_submodules(&repo, &mut dirty_files)?; - - if dirty_files.is_empty() { - return Ok(repo.revparse_single("HEAD")?.id()); - } - color_eyre::eyre::bail!( - "{} files in the working directory contain changes that were \ - not yet committed into git:\n\n{}\n\n\ - commit these changes to continue deployment", - dirty_files.len(), - dirty_files - .iter() - .map(to_string) - .collect::, _>>() - .wrap_err("Error parsing PathBaf")? - .join("\n") - ) -} - -// Helper to collect dirty statuses for a single repo. -fn collect_statuses( - repo: &git2::Repository, - dirty_files: &mut Vec, -) -> near_cli_rs::CliResult { - let mut status_opts = git2::StatusOptions::new(); - // Exclude submodules, as they are being handled manually by recursing - // into each one so that details about specific files can be - // retrieved. - status_opts - .exclude_submodules(true) - .include_ignored(true) - .include_untracked(true); - let repo_statuses = repo.statuses(Some(&mut status_opts)).with_context(|| { - format!( - "Failed to retrieve git status from repo {}", - repo.path().display() - ) - })?; - let workdir = repo.workdir().unwrap(); - let this_dirty = repo_statuses.iter().filter_map(|entry| { - let path = entry.path().expect("valid utf-8 path"); - if path.ends_with("Cargo.lock") || entry.status() == git2::Status::IGNORED { - return None; - } - Some(workdir.join(path)) - }); - dirty_files.extend(this_dirty); - Ok(()) -} - -// Helper to collect dirty statuses while recursing into submodules. -fn status_submodules( - repo: &git2::Repository, - dirty_files: &mut Vec, -) -> near_cli_rs::CliResult { - for submodule in repo.submodules()? { - // Ignore submodules that don't open, they are probably not initialized. - // If its files are required, then the verification step should fail. - if let Ok(sub_repo) = submodule.open() { - status_submodules(&sub_repo, dirty_files)?; - collect_statuses(&sub_repo, dirty_files)?; - } - } - Ok(()) -} - -fn remote_repo_url(contract_path: &camino::Utf8PathBuf) -> color_eyre::Result { - let mut path_cargo_toml = contract_path.clone(); - path_cargo_toml.push("Cargo.toml"); - let cargo_toml = cargo_toml::Manifest::from_slice( - &std::fs::read(&path_cargo_toml) - .wrap_err_with(|| format!("Failed to read file <{path_cargo_toml}>"))?, - ) - .wrap_err("Could not parse 'Cargo.toml'")?; - - let mut remote_repo_url = reqwest::Url::parse( - cargo_toml - .package() - .repository() - .wrap_err("No reference to the remote repository for this contract was found in the file 'Cargo.toml'.\ - \nAdd the value 'repository' to the '[package]' section to continue deployment.")? - )?; - - let path = remote_repo_url.path().trim_end_matches('/'); - - let repo_id = check_repo_state(contract_path)?.to_string(); - - let commit = format!("{path}/commit/{repo_id}"); - - remote_repo_url.set_path(&commit); - log::info!("checking existence of {}", remote_repo_url); - - let mut retries_left = (0..5).rev(); - loop { - let response = reqwest::blocking::get(remote_repo_url.clone())?; - - if retries_left.next().is_none() { - color_eyre::eyre::bail!("Currently, it is not possible to check for remote repository <{remote_repo_url}>. Try again after a while.") - } - - // Check if status is within 100-199. - if response.status().is_informational() { - eprintln!("Transport error.\nPlease wait. The next try to send this query is happening right now ..."); - } - - // Check if status is within 200-299. - if response.status().is_success() { - return Ok(remote_repo_url); - } - - // Check if status is within 300-399. - if response.status().is_redirection() { - return Ok(remote_repo_url); - } - - // Check if status is within 400-499. - if response.status().is_client_error() { - color_eyre::eyre::bail!("Remote repository <{remote_repo_url}> does not exist.") - } - - // Check if status is within 500-599. - if response.status().is_server_error() { - eprintln!("Transport error.\nPlease wait. The next try to send this query is happening right now ..."); - } - - std::thread::sleep(std::time::Duration::from_millis(100)); - } -} diff --git a/cargo-near/src/main.rs b/cargo-near/src/main.rs index dda64a14..caf92580 100644 --- a/cargo-near/src/main.rs +++ b/cargo-near/src/main.rs @@ -1,3 +1,4 @@ +use cargo_near::commands::build_command::INSIDE_DOCKER_ENV_KEY; use colored::Colorize; use interactive_clap::ToCliArgs; use log::Level; @@ -10,7 +11,7 @@ use cargo_near::Cmd; fn main() -> CliResult { let mut builder = env_logger::Builder::from_env(env_logger::Env::default()); - let environment = if std::env::var("NEAR_BUILD_ENVIRONMENT_REF").is_ok() { + let environment = if std::env::var(INSIDE_DOCKER_ENV_KEY).is_ok() { "container".cyan() } else { "host".purple() diff --git a/cargo-near/src/util/mod.rs b/cargo-near/src/util/mod.rs index c425eced..b3b33aca 100644 --- a/cargo-near/src/util/mod.rs +++ b/cargo-near/src/util/mod.rs @@ -160,6 +160,7 @@ where pub struct CompilationArtifact { pub path: Utf8PathBuf, pub fresh: bool, + pub from_docker: bool, } /// Builds the cargo project with manifest located at `manifest_path` and returns the path to the generated artifact. @@ -229,6 +230,7 @@ pub(crate) fn compile_project( (Some(path), None) => Ok(CompilationArtifact { path, fresh: !compile_artifact.fresh, + from_docker: false, }), _ => color_eyre::eyre::bail!( "Compilation resulted in more than one '.{}' target file: {:?}", diff --git a/integration-tests/src/lib.rs b/integration-tests/src/lib.rs index f386eb71..45f0d896 100644 --- a/integration-tests/src/lib.rs +++ b/integration-tests/src/lib.rs @@ -73,8 +73,6 @@ macro_rules! invoke_cargo_near { }, Some(cargo_near::commands::CliNearCommand::Build(cmd)) => { let args = cargo_near::commands::build_command::BuildCommand { - // this is implied by 10 lines below - no_docker: true, no_release: cmd.no_release, no_abi: cmd.no_abi, no_embed_abi: cmd.no_embed_abi, @@ -83,7 +81,11 @@ macro_rules! invoke_cargo_near { manifest_path: Some(cargo_path.into()), color: cmd.color, }; - cargo_near::commands::build_command::build::run(args)?; + std::env::set_var( + cargo_near::commands::build_command::INSIDE_DOCKER_ENV_KEY, + "INTEGRATION_TESTS_NO_DOCKER" + ); + args.run(cargo_near::commands::build_command::BuildContext::Build)?; }, Some(_) => todo!(), None => ()