From 1bd466b298bfe5addc18fcebc3cf6356da5167f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dj8yf0=CE=BCl?= <26653921+dj8yfo@users.noreply.github.com> Date: Sat, 13 Apr 2024 18:46:37 +0300 Subject: [PATCH] feat(source-scan): logging, refactor, image with same `cargo-near` embedded, remove `--no-docker` flag (#144) Following output was generated by using the branch of current pr with [build local `cargo-near` Dockerfile](https://github.com/dj8yfo/cargo-near-image/tree/0.x.x-builer-local-cargo-near) => [image](https://hub.docker.com/r/dj8yfo/sourcescan/tags) on this [repo](https://github.com/dj8yfo/sample_no_workspace/tree/set_metadata) ![Screenshot_20240412_230234](https://github.com/near/cargo-near/assets/26653921/9014a2de-33f8-4ddb-9d7d-d9a78ddb43df) ![Screenshot_20240412_230436](https://github.com/near/cargo-near/assets/26653921/a6f3023b-4858-460c-bbf0-4cdc94ea5bae) ![Screenshot_20240412_230749](https://github.com/near/cargo-near/assets/26653921/36f7f64f-ae50-4620-aa35-81f4db54eed0) --- .github/workflows/test.yml | 2 +- .../src/commands/build_command/build.rs | 4 +- .../src/commands/build_command/docker.rs | 226 ++++++++++++ .../build_command/docker/cloned_repo.rs | 27 ++ .../build_command/docker/git_checks.rs | 139 ++++++++ .../commands/build_command/docker/metadata.rs | 58 ++++ cargo-near/src/commands/build_command/mod.rs | 322 +++--------------- cargo-near/src/commands/deploy/mod.rs | 172 +--------- cargo-near/src/main.rs | 25 +- cargo-near/src/types/metadata.rs | 1 + cargo-near/src/util/mod.rs | 6 +- cargo-near/src/util/print.rs | 17 +- integration-tests/src/lib.rs | 19 +- 13 files changed, 557 insertions(+), 461 deletions(-) create mode 100644 cargo-near/src/commands/build_command/docker.rs create mode 100644 cargo-near/src/commands/build_command/docker/cloned_repo.rs create mode 100644 cargo-near/src/commands/build_command/docker/git_checks.rs create mode 100644 cargo-near/src/commands/build_command/docker/metadata.rs diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cbbe741e..7bc7b475 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -66,7 +66,7 @@ jobs: run: sudo apt-get update && sudo apt-get install --assume-yes libudev-dev - name: Run tests - run: cargo test --workspace --verbose + run: cargo test --workspace lint: runs-on: ubuntu-latest 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..b4929e33 --- /dev/null +++ b/cargo-near/src/commands/build_command/docker.rs @@ -0,0 +1,226 @@ +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() + ); + println!( + "{}", + "You can choose to opt out into non-docker build behaviour by using `--no-docker` flag.".cyan() + ); + + 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 dc4033b3..7eea4c5a 100644 --- a/cargo-near/src/commands/build_command/mod.rs +++ b/cargo-near/src/commands/build_command/mod.rs @@ -1,30 +1,18 @@ use std::ops::Deref; -use std::process::{id, Command}; -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 + /// Build contract on host system and without embedding SourceScan NEP-330 metadata #[interactive_clap(long)] - pub no_docker: bool, + no_docker: bool, /// Build contract in debug mode, without optimizations and bigger is size #[interactive_clap(long)] pub no_release: bool, @@ -52,6 +40,50 @@ 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(&self) -> bool { + std::env::var(INSIDE_DOCKER_ENV_KEY).is_ok() || self.no_docker + } +} + +impl From for BuildCommand { + fn from(value: CliBuildCommand) -> Self { + Self { + no_docker: value.no_docker, + no_release: value.no_release, + no_abi: value.no_abi, + no_embed_abi: value.no_embed_abi, + no_doc: value.no_doc, + out_dir: value.out_dir, + manifest_path: value.manifest_path, + color: value.color, + } + } +} + #[derive(Debug, Clone)] pub struct BuildCommandlContext; @@ -70,259 +102,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::debug!("ClonedRepo.contract_path: {:?}", contract_path,); - - let tmp_contract_dir = tempfile::tempdir()?; - let tmp_contract_path = tmp_contract_dir.path().to_path_buf(); - log::debug!("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_metadata(manifest_path: camino::Utf8PathBuf) -> color_eyre::eyre::Result { - log::debug!( - "crate in cloned location manifest path : {:?}", - manifest_path - ); - let crate_metadata = util::handle_step("Collecting cargo project metadata...", || { - CrateMetadata::collect(CargoManifestPath::try_from(manifest_path)?) - })?; - log::trace!("crate metadata : {:#?}", crate_metadata); - Ok(crate_metadata) -} - -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!( - "{}", - format!("reproducible build metadata: {:#?}", build_meta).green() - ); - Ok(build_meta) -} - -pub fn docker_run(args: BuildCommand) -> color_eyre::eyre::Result { - 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") - } - let color = args - .color - .clone() - .unwrap_or(crate::common::ColorPreference::Auto) - .to_string(); - cargo_args.extend(&["--color", &color]); - - let mut cloned_repo = clone_repo(&args)?; - - 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()? - }; - let cargo_metadata = get_metadata(cargo_toml_path)?; - let docker_build_meta = get_docker_build_meta(&cargo_metadata)?; - - // 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, - &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" - )); - } - }; - - if status.success() { - // 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" { - cloned_repo.contract_path.push("contract.wasm"); - std::fs::copy::( - entry.path(), - cloned_repo.contract_path.clone(), - )?; - - return Ok(cloned_repo.contract_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 666b0721..ed899da6 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()) - })? - }; - - eprintln!( - "\nThe URL of the remote repository:\n{}\n", - remote_repo_url(&contract_path)? - ); - - let file_path = if !scope.build_command_args.no_docker { - 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 { @@ -83,16 +66,7 @@ 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, - no_doc: cli_build_command_args.no_doc, - out_dir: cli_build_command_args.out_dir.clone(), - manifest_path: cli_build_command_args.manifest_path.clone(), - color: cli_build_command_args.color.clone(), - } + build_command::BuildCommand::from(cli_build_command_args.clone()) } else { build_command::BuildCommand::default() }; @@ -148,137 +122,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); - - 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 7e7821f2..caf92580 100644 --- a/cargo-near/src/main.rs +++ b/cargo-near/src/main.rs @@ -1,4 +1,7 @@ +use cargo_near::commands::build_command::INSIDE_DOCKER_ENV_KEY; +use colored::Colorize; use interactive_clap::ToCliArgs; +use log::Level; pub use near_cli_rs::CliResult; use std::env; use std::io::Write; @@ -7,16 +10,32 @@ 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(INSIDE_DOCKER_ENV_KEY).is_ok() { + "container".cyan() + } else { + "host".purple() + }; + builder - .format(|buf, record| { + .format(move |buf, record| { + let level = format!("[{}]", record.level()); + let level = match record.level() { + Level::Error => level.red(), + Level::Warn => level.yellow(), + Level::Info => level.cyan(), + Level::Debug => level.truecolor(100, 100, 100), + Level::Trace => level.truecolor(200, 200, 200), + }; let ts = buf.timestamp_seconds(); writeln!( buf, - "{}:{} {} [{}] - {}", + " {}-[{}] {}:{} {} - {}", + level, + environment, record.file().unwrap_or("unknown"), record.line().unwrap_or(0), ts, - record.level(), record.args() ) }) diff --git a/cargo-near/src/types/metadata.rs b/cargo-near/src/types/metadata.rs index daf33c8d..a588f2d6 100644 --- a/cargo-near/src/types/metadata.rs +++ b/cargo-near/src/types/metadata.rs @@ -40,6 +40,7 @@ impl CrateMetadata { manifest_path, raw_metadata: metadata, }; + log::trace!("crate metadata : {:#?}", crate_metadata); Ok(crate_metadata) } } diff --git a/cargo-near/src/util/mod.rs b/cargo-near/src/util/mod.rs index 3a13c716..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. @@ -193,10 +194,6 @@ pub(crate) fn compile_project( } } } - log::debug!( - "compile project args: {:?}", - [&["--message-format=json-render-diagnostics"], args].concat() - ); let artifacts = invoke_cargo( "build", @@ -233,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/cargo-near/src/util/print.rs b/cargo-near/src/util/print.rs index 9d32aa66..49ba0db9 100644 --- a/cargo-near/src/util/print.rs +++ b/cargo-near/src/util/print.rs @@ -4,12 +4,12 @@ pub(crate) fn handle_step(msg: &str, f: F) -> color_eyre::eyre::Result where F: FnOnce() -> color_eyre::eyre::Result, { - eprint!(" {} {}", "•".bold().cyan(), msg); + eprintln!(" {} {}", "•".bold().cyan(), msg); let result = f(); if result.is_ok() { - eprintln!("{}", "done".bold().green()); + eprintln!(" {} {}\n", "•".bold().cyan(), "done".bold().green()); } else { - eprintln!("{}", "failed".bold().red()); + eprintln!(" {} {}\n", "•".bold().cyan(), "failed".bold().red()); } result } @@ -21,3 +21,14 @@ pub(crate) fn print_step(msg: &str) { pub(crate) fn print_success(msg: &str) { eprintln!(" {} {}", "✓".bold().green(), msg); } + +pub(crate) fn indent_string(msg: &str) -> String { + let indent = " "; + let newline_indent = format!("\n{}", indent); + let lines = msg.split('\n').collect::>(); + let mut res = String::new(); + res.push_str(indent); + + res.push_str(&lines.join(&newline_indent)); + res +} diff --git a/integration-tests/src/lib.rs b/integration-tests/src/lib.rs index 7dbd3f48..90be01f4 100644 --- a/integration-tests/src/lib.rs +++ b/integration-tests/src/lib.rs @@ -72,17 +72,12 @@ macro_rules! invoke_cargo_near { cargo_near::commands::abi_command::abi::run(args)?; }, Some(cargo_near::commands::CliNearCommand::Build(cmd)) => { - let args = cargo_near::commands::build_command::BuildCommand { - no_docker: cmd.no_docker, - no_release: cmd.no_release, - no_abi: cmd.no_abi, - no_embed_abi: cmd.no_embed_abi, - no_doc: cmd.no_doc, - out_dir: cmd.out_dir, - manifest_path: Some(cargo_path.into()), - color: cmd.color, + let args = { + let mut args = cargo_near::commands::build_command::BuildCommand::from(cmd) ; + args.manifest_path = Some(cargo_path.into()); + args }; - cargo_near::commands::build_command::build::run(args)?; + args.run(cargo_near::commands::build_command::BuildContext::Build)?; }, Some(_) => todo!(), None => () @@ -164,8 +159,8 @@ pub struct BuildResult { #[macro_export] macro_rules! build_with { ($(Cargo: $cargo_path:expr;)? $(Vars: $cargo_vars:expr;)? $(Opts: $cli_opts:expr;)? Code: $($code:tt)*) => {{ - let opts = "cargo near build"; - $(let opts = format!("cargo near build {}", $cli_opts);)?; + let opts = "cargo near build --no-docker"; + $(let opts = format!("cargo near build --no-docker {}", $cli_opts);)?; let result_dir = $crate::invoke_cargo_near! { $(Cargo: $cargo_path;)? $(Vars: $cargo_vars;)? Opts: &opts;