From a63fb95776f02698085af47413eb9ca1ad66833c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dj8yf0=CE=BCl?= <26653921+dj8yfo@users.noreply.github.com> Date: Tue, 17 Dec 2024 13:21:00 +0200 Subject: [PATCH] feat: reproducible choice interactive (#262) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit this will look now like the following ```bash ❯ : cargo near build non-reproducible-wasm --help Runs on current filesystem state without many restrictions Usage: cargo-near near build non-reproducible-wasm [OPTIONS] Options: --locked --no-release --no-abi --no-embed-abi --no-doc --no-wasmopt --out-dir --manifest-path --features --no-default-features --color [possible values: auto, always, never] --env -h, --help Print help -V, --version Print version ``` ```bash ❯ : cargo near build reproducible-wasm --help Requires `docker` config added and (git)committed to Cargo.toml, runs on clean (git)working tree state Usage: cargo-near near build reproducible-wasm [OPTIONS] Options: --no-locked --out-dir --manifest-path --color [possible values: auto, always, never] -h, --help Print help -V, --version Print version ``` --- this is pending - [x] deploy cmd dispatch in current pr - [x] logic to enforce correct `container_build_command` for newer images (ones, that will be built after release of this pr) - [x] prettier indents in interactive help msgs in current pr - [x] separate pr onto current pr as base branch with tests fixed as per api change (#263) - [x] testing of this pr on a few common cases (sample-crate, factory-rust, neardevhub-contract) (in comment https://github.com/near/cargo-near/pull/262#issuecomment-2529296185) - [x] separate pr onto current pr as base branch with `.github/workflows` change /testing (#264) - [x] separate pr onto current pr as base branch with docs/README update (#265) - [ ] merge --------- Co-authored-by: dj8yf0μl --- .../crontab_new_template_renewal.yml | 4 +- Cargo.lock | 5 +- README.md | 75 ++++-- .../near/docker_build/docker_checks/mod.rs | 11 - cargo-near-build/src/near/docker_build/mod.rs | 13 +- cargo-near-build/src/types/cargo/metadata.rs | 4 +- .../src/types/near/build/input/mod.rs | 20 +- .../near/docker_build/compute_command.rs | 165 ++------------ .../src/types/near/docker_build/metadata.rs | 34 +-- .../src/types/near/docker_build/mod.rs | 31 +-- .../src/commands/{abi_command => abi}/mod.rs | 18 +- .../actions/non_reproducible_wasm/mod.rs | 215 ++++++++++++++++++ .../build/actions/reproducible_wasm/mod.rs | 76 +++++++ cargo-near/src/commands/build/mod.rs | 50 ++++ cargo-near/src/commands/build_command/mod.rs | 198 ---------------- .../deploy/actions/non_reproducible_wasm.rs | 149 ++++++++++++ .../deploy/actions/reproducible_wasm.rs | 158 +++++++++++++ cargo-near/src/commands/deploy/mod.rs | 152 ++----------- cargo-near/src/commands/mod.rs | 14 +- cargo-near/src/commands/new/mod.rs | 2 +- .../.github/workflows/deploy-production.yml | 2 +- .../.github/workflows/deploy-staging.yml | 2 +- ...argo.toml.template => Cargo.template.toml} | 0 .../new/new-project-template/README.md | 2 +- cargo-near/src/lib.rs | 2 +- cargo-near/src/main.rs | 39 ++-- docs/image_and_digest_pinpoint.png | Bin 0 -> 31223 bytes integration-tests/Cargo.toml | 4 +- integration-tests/src/lib.rs | 205 +++++++++++------ integration-tests/tests/cargo/mod.rs | 4 +- 30 files changed, 966 insertions(+), 688 deletions(-) rename cargo-near/src/commands/{abi_command => abi}/mod.rs (81%) create mode 100644 cargo-near/src/commands/build/actions/non_reproducible_wasm/mod.rs create mode 100644 cargo-near/src/commands/build/actions/reproducible_wasm/mod.rs create mode 100644 cargo-near/src/commands/build/mod.rs delete mode 100644 cargo-near/src/commands/build_command/mod.rs create mode 100644 cargo-near/src/commands/deploy/actions/non_reproducible_wasm.rs create mode 100644 cargo-near/src/commands/deploy/actions/reproducible_wasm.rs rename cargo-near/src/commands/new/new-project-template/{Cargo.toml.template => Cargo.template.toml} (100%) create mode 100644 docs/image_and_digest_pinpoint.png diff --git a/.github/workflows/crontab_new_template_renewal.yml b/.github/workflows/crontab_new_template_renewal.yml index ab441145..42625d5c 100644 --- a/.github/workflows/crontab_new_template_renewal.yml +++ b/.github/workflows/crontab_new_template_renewal.yml @@ -30,7 +30,7 @@ jobs: let record = http get "https://hub.docker.com/v2/namespaces/sourcescan/repositories/cargo-near/tags" | get results | first; let mod_content = ( - open cargo-near/src/commands/new/new-project-template/Cargo.toml.template --raw | lines + open cargo-near/src/commands/new/new-project-template/Cargo.template.toml --raw | lines | each { |line| if ($line | str starts-with "image = ") { $'image = "sourcescan/cargo-near:($record.name)"' @@ -44,7 +44,7 @@ jobs: | to text ); - $mod_content | save -f cargo-near/src/commands/new/new-project-template/Cargo.toml.template + $mod_content | save -f cargo-near/src/commands/new/new-project-template/Cargo.template.toml git diff diff --git a/Cargo.lock b/Cargo.lock index 3e53846f..24f34c63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -572,7 +572,6 @@ dependencies = [ "tokio", "toml", "tracing", - "tracing-subscriber", "zstd 0.13.2", ] @@ -2687,9 +2686,9 @@ dependencies = [ [[package]] name = "near-cli-rs" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6696da4e6fb1a4b7f37a2ca2a25bd6eecf54a9d84fde59ea3a67d53e6ee48002" +checksum = "7961d3f890e0823a1737f1732a8019c6f74ac4cf7549b76722ad9c80c6769d1c" dependencies = [ "bip39", "bs58 0.5.1", diff --git a/README.md b/README.md index 3d9126c2..a2b47954 100644 --- a/README.md +++ b/README.md @@ -99,26 +99,57 @@ cargo near build Builds a NEAR smart contract along with its [ABI](https://github.com/near/abi) (while in the directory containing contract's Cargo.toml). -By default, this runs a reproducible build in a [Docker](https://docs.docker.com/) container, which: +Running the above command opens a menu with following variants: + +### `non-reproducible-wasm` + +**Recommended variant for use during local development.** + +This is a regular build, which behaves much like and is a thin wrapper around a regular `cargo build --target wasm32-unknown-unknown --release`. + +Additional flags for build configuration can be looked up by + +```bash +cargo near build non-reproducible-wasm --help +``` +if needed. + +### `reproducible-wasm` + +**Recommended variant for the production releases.** + +This variant runs a reproducible build in a [Docker](https://docs.docker.com/) container, which: 1. runs against source code version, committed to git, ignoring any uncommitted changes 2. requires that `Cargo.lock` of project is created (e.g. via `cargo update`) and added to git. - this enables `--locked` build by downstream `cargo` command. -3. will use configuration in [`[package.metadata.near.reproducible_build]`](https://github.com/near/cargo-near/blob/main/cargo-near/src/commands/new/new-project-template/Cargo.toml.template#L14-L25) - section of contract's `Cargo.toml` and [`package.repository`](https://github.com/near/cargo-near/blob/main/cargo-near/src/commands/new/new-project-template/Cargo.toml.template#L9) field +3. will use configuration in [`[package.metadata.near.reproducible_build]`](https://github.com/near/cargo-near/blob/main/cargo-near/src/commands/new/new-project-template/Cargo.template.toml#L14-L25) + section of contract's `Cargo.toml` and [`package.repository`](https://github.com/near/cargo-near/blob/main/cargo-near/src/commands/new/new-project-template/Cargo.template.toml#L9) field - default values for this section can also be found in `Cargo.toml` of template project, generated by `cargo near new` -Important flags: +**What's a reproducible build in context of NEAR?** +Why is it needed? Explanation of these points and a step-by-step tutorial is present at [SourceScan/verification-guide](https://github.com/SourceScan/verification-guide). -1. `--no-docker` - - flag can be used to perform a regular build with rust toolchain installed onto host, running the `cargo-near` cli. - - *NO*-Docker builds run against actual state of code in filesystem and not against a version, committed to source control. - -2. `--no-locked` - - flag is allowed in *NO*-Docker builds, e.g. to generate a `Cargo.lock` *and* simultaneously build the contract. - - flag is allowed in Docker builds, but - - such builds are not reproducible due to potential update of dependencies and compiled `wasm` mismatch as the result. +
+ Additional (optional) details on possible [package.metadata.near.reproducible_build] configuration

+ +1. available images can be found by this link https://hub.docker.com/r/sourcescan/cargo-near/tags + - [`image`](https://github.com/near/cargo-near/blob/main/cargo-near/src/commands/new/new-project-template/Cargo.template.toml#L18) and [`image_digest`](https://github.com/near/cargo-near/blob/main/cargo-near/src/commands/new/new-project-template/Cargo.template.toml#L19) are straightforward to configure: + ![image_and_digest_pinpoint](./docs/image_and_digest_pinpoint.png) +2. flags of build command, run inside of docker container, can be configured, if needed, by changing [`container_build_command`](https://github.com/near/cargo-near/blob/main/cargo-near/src/commands/new/new-project-template/Cargo.template.toml#L29) field + - base `container_build_command` for images starting with **sourcescan/cargo-near:0.13.0-rust-1.83.0** and after it is `["cargo", "near", "build", "non-reproducible-wasm", "--locked"]`, where the `--locked` flag is required + - base `container_build_command` for images prior to **sourcescan/cargo-near:0.13.0-rust-1.83.0** is `["cargo", "near", "build"]` + - additional flags, if needed, can be looked up on + - `cargo near build non-reproducible-wasm --help` for newer/latest images + - `cargo near build --help` for older ones + - running `docker run -it sourcescan/cargo-near:0.11.0-rust-1.82.0` (or another specific image) and checking the `--help` message of exact `cargo-near` in container may be helpful when in doubt +3. `cargo near` allows parameterizing build with values of environment variables, present at the time of the build and not present in a contract's source code, + by specifying their names in [`passed_env`](https://github.com/near/cargo-near/blob/main/cargo-near/src/commands/new/new-project-template/Cargo.template.toml#L24) array + - supported by **sourcescan/cargo-near:0.10.1-rust-1.82.0** image or later images + - SourceScan/Nearblocks does not support verifying such contracts with additional parameters present in their metadata yet + +

--- @@ -146,22 +177,22 @@ cargo near deploy Builds the smart contract (equivalent to `cargo near build`) and guides you to deploy it to the blockchain. -By default, this runs a reproducible build in a Docker container. +Similar to `build`, running the above command opens a menu with following variants: + +### `build-non-reproducible-wasm` + +This forwards to [non-reproducible-wasm](#non-reproducible-wasm) variant of `build` command. + +### `build-reproducible-wasm` + +This forwards to [reproducible-wasm](#reproducible-wasm) variant of `build` command. `deploy` command from Docker build requires that contract's source code: 1. doesn't have any modified tracked files, any staged changes or any untracked content. 2. has been pushed to remote repository, identified by - [`package.repository`](https://github.com/near/cargo-near/blob/main/cargo-near/src/commands/new/new-project-template/Cargo.toml.template#L9). - -Important flags: - -1. `--no-docker` - - flag can be used to perform a regular *NO*-Docker build *and* deploy. - - Similar to `build` command, in this case none of the git-related concerns and restrictions apply. + [`package.repository`](https://github.com/near/cargo-near/blob/main/cargo-near/src/commands/new/new-project-template/Cargo.template.toml#L9). -2. `--no-locked` - - flag is declined for deploy, due to its effects on `build` result ## Contribution diff --git a/cargo-near-build/src/near/docker_build/docker_checks/mod.rs b/cargo-near-build/src/near/docker_build/docker_checks/mod.rs index df83f188..42830c6a 100644 --- a/cargo-near-build/src/near/docker_build/docker_checks/mod.rs +++ b/cargo-near-build/src/near/docker_build/docker_checks/mod.rs @@ -19,7 +19,6 @@ pub fn handle_command_io_error( println!(); println!("{}", "`docker` executable isn't available".yellow()); print_installation_links(); - print_non_docker_suggestion(); Err(report) } Err(io_err) => { @@ -33,7 +32,6 @@ pub fn handle_command_io_error( .yellow() ); println!("{}", format!("Error `{:?}`", io_err).yellow()); - print_non_docker_suggestion(); Err(report) } } @@ -127,13 +125,4 @@ pub fn print_command_status(status: std::process::ExitStatus, command: std::proc ) .yellow() ); - print_non_docker_suggestion(); -} - -fn print_non_docker_suggestion() { - println!( - "{}", - "You can choose to opt out into non-docker build behaviour by using `--no-docker` flag." - .cyan() - ); } diff --git a/cargo-near-build/src/near/docker_build/mod.rs b/cargo-near-build/src/near/docker_build/mod.rs index cf47bbaf..731af488 100644 --- a/cargo-near-build/src/near/docker_build/mod.rs +++ b/cargo-near-build/src/near/docker_build/mod.rs @@ -1,5 +1,7 @@ use std::process::{Command, ExitStatus}; +use colored::Colorize; + use crate::docker::DockerBuildOpts; use crate::types::near::build::input::BuildContext; use crate::types::near::build::output::CompilationArtifact; @@ -33,10 +35,13 @@ pub fn run(opts: DockerBuildOpts) -> eyre::Result { }, )?; - let docker_build_meta = - pretty_print::handle_step("Parsing and validating `Cargo.toml` metadata...", || { - metadata::ReproducibleBuild::parse(cloned_repo.crate_metadata()) - })?; + let docker_build_meta = pretty_print::handle_step( + &format!( + "Parsing and validating `{}` section of contract's `Cargo.toml` ...", + "[package.metadata.near.reproducible_build]".magenta() + ), + || metadata::ReproducibleBuild::parse(cloned_repo.crate_metadata()), + )?; if let BuildContext::Deploy { skip_git_remote_check, diff --git a/cargo-near-build/src/types/cargo/metadata.rs b/cargo-near-build/src/types/cargo/metadata.rs index d4933ce5..97dff376 100644 --- a/cargo-near-build/src/types/cargo/metadata.rs +++ b/cargo-near-build/src/types/cargo/metadata.rs @@ -170,7 +170,9 @@ fn get_cargo_metadata( ); println!( "{}", - "You can choose to disable `--locked` flag for downstream `cargo` command with `--no-locked` flag.".cyan() + "You can choose to disable `--locked` flag for downstream `cargo` command \ + by adding `--no-locked` flag OR by removing `--locked` flag" + .cyan() ); thread::sleep(Duration::new(5, 0)); return Err(cargo_metadata::Error::CargoMetadata { diff --git a/cargo-near-build/src/types/near/build/input/mod.rs b/cargo-near-build/src/types/near/build/input/mod.rs index ba60b733..f2998731 100644 --- a/cargo-near-build/src/types/near/build/input/mod.rs +++ b/cargo-near-build/src/types/near/build/input/mod.rs @@ -88,7 +88,12 @@ impl Default for CliDescription { fn default() -> Self { Self { cli_name_abi: "cargo-near".into(), - cli_command_prefix: vec!["cargo".into(), "near".into(), "build".into()], + cli_command_prefix: vec![ + "cargo".into(), + "near".into(), + "build".into(), + "non-reproducible-wasm".into(), + ], } } } @@ -97,13 +102,16 @@ impl Opts { /// this is just 1-to-1 mapping of each struct's field to a cli flag /// in order of fields, as specified in struct's definition. /// `Default` implementation corresponds to plain `cargo near build` command without any args - pub(crate) fn get_cli_command_for_lib_context(&self) -> Vec { + pub fn get_cli_command_for_lib_context(&self) -> Vec { let cargo_args = self.cli_description.cli_command_prefix.clone(); let mut cargo_args: Vec<&str> = cargo_args.iter().map(|ele| ele.as_str()).collect(); - if self.no_locked { - cargo_args.push("--no-locked"); + // this logical NOT is needed to avoid writing manually `Default` trait impl for `Opts` + // with `self.locked` field and to keep default (if nothing is specified) to *locked* behavior + // which is a desired default for [crate::extended::build] functionality + if !self.no_locked { + cargo_args.push("--locked"); } - // `no_docker` field isn't present + if self.no_release { cargo_args.push("--no-release"); } @@ -220,6 +228,8 @@ mod tests { assert_eq!(opts.get_cli_command_for_lib_context(), ["cargo".to_string(), "near".to_string(), "build".to_string(), + "non-reproducible-wasm".to_string(), + "--locked".to_string(), "--env".to_string(), "KEY=VALUE".to_string(), "--env".to_string(), diff --git a/cargo-near-build/src/types/near/docker_build/compute_command.rs b/cargo-near-build/src/types/near/docker_build/compute_command.rs index 3318715c..7bb88143 100644 --- a/cargo-near-build/src/types/near/docker_build/compute_command.rs +++ b/cargo-near-build/src/types/near/docker_build/compute_command.rs @@ -3,26 +3,25 @@ use colored::Colorize; use super::metadata; impl super::Opts { - const BUILD_COMMAND_CLI_CONFIG_ERR: &'static str = "flag cannot be used, when `container_build_command` is configured from `[package.metadata.near.reproducible_build]` in Cargo.toml"; - pub fn get_cli_build_command_in_docker( &self, docker_build_meta: &metadata::ReproducibleBuild, ) -> eyre::Result> { - if let Some(manifest_command) = docker_build_meta.container_build_command.as_ref() { - self.append_env_suffix( - manifest_command.clone(), - docker_build_meta.passed_env.clone(), - ) - } else { - println!( - "{}", - "configuring `container_build_command` from cli args, passed to current command" - .cyan() - ); - println!(); - Ok(self.passthrough_some_opts_into_docker_cmd()) - } + let Some(manifest_command) = docker_build_meta.container_build_command.as_ref() else { + return Err(eyre::eyre!( + "`container_build_command` is expected to be non-empty (after validation)" + )); + }; + println!( + "{}`{}`{}", + "using `container_build_command` from ".cyan(), + "[package.metadata.near.reproducible_build]".magenta(), + " in Cargo.toml".cyan() + ); + self.append_env_suffix( + manifest_command.clone(), + docker_build_meta.passed_env.clone(), + ) } fn append_env_suffix( @@ -30,7 +29,6 @@ impl super::Opts { mut manifest_command: Vec, passed_env: Option>, ) -> eyre::Result> { - self.check_flag_conflicts_with_manifest_command()?; if let Some(passed_env) = passed_env { let suffix_env = passed_env .into_iter() @@ -63,137 +61,4 @@ impl super::Opts { Ok(manifest_command) } - - fn passthrough_some_opts_into_docker_cmd(&self) -> Vec { - let mut cli_args: Vec = vec![]; - // NOTE: not passing through `no_locked` to cmd in container, - // an invisible Cargo.lock was generated by implicit `cargo metadata` anyway - // if self.no_locked { - // no-op - // } - - cli_args.extend(self.no_release.then_some("--no-release".into())); - cli_args.extend(self.no_abi.then_some("--no-abi".into())); - cli_args.extend(self.no_embed_abi.then_some("--no-embed-abi".into())); - cli_args.extend(self.no_doc.then_some("--no-doc".into())); - cli_args.extend(self.no_wasmopt.then_some("--no-wasmopt".into())); - - if let Some(ref features) = self.features { - cli_args.extend(["--features".into(), features.clone()]); - } - cli_args.extend( - self.no_default_features - .then_some("--no-default-features".into()), - ); - if let Some(ref color) = self.color { - cli_args.extend(["--color".into(), color.to_string()]); - } - cli_args.extend(self.env.clone().into_iter().flat_map(|(key, value)| { - let equal_pair = [key, value].join("="); - ["--env".to_string(), equal_pair] - })); - - let mut cli_command_prefix = self.cli_description.cli_command_prefix.clone(); - cli_command_prefix.extend(cli_args); - cli_command_prefix - } - fn check_flag_conflicts_with_manifest_command(&self) -> eyre::Result<()> { - // NOTE: `--no-locked` is allowed for docker builds - // if self.no_locked { - // no-op - // } - if self.no_release { - return Err(eyre::eyre!(format!( - "`{}` {}", - "--no-release", - Self::BUILD_COMMAND_CLI_CONFIG_ERR - ))); - } - if self.no_abi { - return Err(eyre::eyre!(format!( - "`{}` {}", - "--no-abi", - Self::BUILD_COMMAND_CLI_CONFIG_ERR - ))); - } - if self.no_embed_abi { - return Err(eyre::eyre!(format!( - "`{}` {}", - "--no-embed-abi", - Self::BUILD_COMMAND_CLI_CONFIG_ERR - ))); - } - if self.no_doc { - return Err(eyre::eyre!(format!( - "`{}` {}", - "--no-doc", - Self::BUILD_COMMAND_CLI_CONFIG_ERR - ))); - } - if self.no_wasmopt { - return Err(eyre::eyre!(format!( - "`{}` {}", - "--no-wasmopt ", - Self::BUILD_COMMAND_CLI_CONFIG_ERR - ))); - } - if self.features.is_some() { - return Err(eyre::eyre!(format!( - "`{}` {}", - "--features", - Self::BUILD_COMMAND_CLI_CONFIG_ERR - ))); - } - if self.no_default_features { - return Err(eyre::eyre!(format!( - "`{}` {}", - "--no-default-features", - Self::BUILD_COMMAND_CLI_CONFIG_ERR - ))); - } - if !self.env.is_empty() { - return Err(eyre::eyre!(format!( - "`{}` {}\n{}", - "--env", - Self::BUILD_COMMAND_CLI_CONFIG_ERR, - "You can specify environment vars in `passed_env` list in `[package.metadata.near.reproducible_build]` instead" - ))); - } - Ok(()) - } -} - -#[cfg(test)] -mod tests { - #[test] - fn test_passthrough_some_opts_into_docker_cmd() { - let opts = crate::docker::DockerBuildOpts { - no_release: true, - no_abi: true, - no_embed_abi: true, - no_doc: true, - features: Some("cool_feature".into()), - no_default_features: true, - color: Some(crate::ColorPreference::Always), - ..Default::default() - }; - - assert_eq!( - opts.passthrough_some_opts_into_docker_cmd(), - vec![ - "cargo".to_string(), - "near".to_string(), - "build".to_string(), - "--no-release".to_string(), - "--no-abi".to_string(), - "--no-embed-abi".to_string(), - "--no-doc".to_string(), - "--features".to_string(), - "cool_feature".to_string(), - "--no-default-features".to_string(), - "--color".to_string(), - "always".to_string() - ], - ); - } } diff --git a/cargo-near-build/src/types/near/docker_build/metadata.rs b/cargo-near-build/src/types/near/docker_build/metadata.rs index d884f272..12e64e8b 100644 --- a/cargo-near-build/src/types/near/docker_build/metadata.rs +++ b/cargo-near-build/src/types/near/docker_build/metadata.rs @@ -99,12 +99,14 @@ impl ReproducibleBuild { Ok(()) } fn validate_container_build_command(&self) -> eyre::Result<()> { + let build_command = self.container_build_command.clone().ok_or(eyre::eyre!( + "`container_build_command` field not found! It is required since 0.13.0 version of `cargo-near`" + ))?; let is_cargo_near = { - let build_command = self.container_build_command.clone().unwrap_or_default(); Some("cargo") == build_command.first().map(AsRef::as_ref) && Some("near") == build_command.get(1).map(AsRef::as_ref) }; - for command_token in self.container_build_command.clone().unwrap_or_default() { + for command_token in build_command { if command_token .chars() .any(|c| !c.is_ascii() || c.is_ascii_control() || c.is_ascii_whitespace()) @@ -116,6 +118,8 @@ impl ReproducibleBuild { "`container_build_command`: string token contains invalid characters", )); } + // for versions of cargo-near inside of container <0.13 + // versions >=0.13 require `--locked` flag instead, but this isn't validated if is_cargo_near && command_token == "--no-locked" { return Err(eyre::eyre!( "{}:\n{}", @@ -181,13 +185,6 @@ impl ReproducibleBuild { self.validate_if_unknown_keys_present()?; self.validate_repository()?; - if self.passed_env.is_some() && self.container_build_command.is_none() { - return Err(eyre::eyre!( - "{}: \n{}", - "Malformed `[package.metadata.near.reproducible_build]` in Cargo.toml", - "using optional `passed_env` field requires that `container_build_command` is set too", - )); - } Ok(()) } pub fn parse(cargo_metadata: &CrateMetadata) -> eyre::Result { @@ -199,26 +196,22 @@ impl ReproducibleBuild { let mut build_meta: ReproducibleBuild = match build_meta_value { None => { - println!( - "{}{}{}", - "An error with missing ".yellow(), - "`[package.metadata.near.reproducible_build]`".magenta(), - " in Cargo.toml has been encountered...".yellow() - ); println!( "{}", - "You can choose to disable docker build with `--no-docker` flag...".cyan() + "Metadata section in contract's Cargo.toml, \ + that is prerequisite for reproducible builds, has not been found..." + .yellow() ); thread::sleep(Duration::new(7, 0)); println!(); println!( "{}{}{}", - "Alternatively you can add and commit ".cyan(), + "You can add and commit ".cyan(), "`[package.metadata.near.reproducible_build]` ".magenta(), "to your contract's Cargo.toml:".cyan() ); println!("{}{}", "- default values for the section can be found at ".cyan(), - "https://github.com/near/cargo-near/blob/main/cargo-near/src/commands/new/new-project-template/Cargo.toml.template#L14-L25".magenta()); + "https://github.com/near/cargo-near/blob/main/cargo-near/src/commands/new/new-project-template/Cargo.template.toml#L14-L29".magenta()); println!( "{}{}", "- the same can also be found in Cargo.toml of template project, generated by " @@ -249,11 +242,6 @@ impl ReproducibleBuild { .transpose()?; build_meta.validate()?; println!("{} {}", "reproducible build metadata:".green(), build_meta); - if build_meta.container_build_command.is_some() { - println!( - "{}", "using `container_build_command` from `[package.metadata.near.reproducible_build]` in Cargo.toml".cyan() - ); - } Ok(build_meta) } pub fn concat_image(&self) -> String { diff --git a/cargo-near-build/src/types/near/docker_build/mod.rs b/cargo-near-build/src/types/near/docker_build/mod.rs index 2261f819..a981377b 100644 --- a/cargo-near-build/src/types/near/docker_build/mod.rs +++ b/cargo-near-build/src/types/near/docker_build/mod.rs @@ -1,5 +1,5 @@ use crate::types::cargo::manifest_path::ManifestPath; -use crate::{CliDescription, ColorPreference}; +use crate::ColorPreference; use super::build::input::BuildContext; @@ -15,42 +15,13 @@ pub struct Opts { /// disable implicit `--locked` flag for all `cargo` commands, enabled by default #[builder(default)] pub no_locked: bool, - /// Build contract in debug mode, without optimizations and bigger in size - #[builder(default)] - pub no_release: bool, - /// Do not generate ABI for the contract - #[builder(default)] - pub no_abi: bool, - /// Do not embed the ABI in the contract binary - #[builder(default)] - pub no_embed_abi: bool, - /// Do not include rustdocs in the embedded ABI - #[builder(default)] - pub no_doc: bool, - /// do not run `wasm-opt -O` on the generated output as a post-step - #[builder(default)] - pub no_wasmopt: bool, /// Copy final artifacts to this directory pub out_dir: Option, /// Path to the `Cargo.toml` of the contract to build pub manifest_path: Option, - /// Set compile-time feature flags. - #[builder(into)] - pub features: Option, - /// Disables default feature flags. - #[builder(default)] - pub no_default_features: bool, /// Coloring: auto, always, never; /// assumed to be auto when `None` pub color: Option, - /// description of cli command, where [BuildOpts](crate::BuildOpts) are being used from, either real - /// or emulated - #[builder(default)] - pub cli_description: CliDescription, - /// additional environment key-value pairs, that should be passed to underlying - /// build commands - #[builder(default)] - pub env: Vec<(String, String)>, #[builder(default)] pub context: BuildContext, } diff --git a/cargo-near/src/commands/abi_command/mod.rs b/cargo-near/src/commands/abi/mod.rs similarity index 81% rename from cargo-near/src/commands/abi_command/mod.rs rename to cargo-near/src/commands/abi/mod.rs index bcc8bc32..28ff252c 100644 --- a/cargo-near/src/commands/abi_command/mod.rs +++ b/cargo-near/src/commands/abi/mod.rs @@ -3,10 +3,10 @@ use cargo_near_build::abi::AbiOpts; #[derive(Debug, Clone, interactive_clap::InteractiveClap)] #[interactive_clap(input_context = near_cli_rs::GlobalContext)] #[interactive_clap(output_context = AbiCommandlContext)] -pub struct AbiCommand { - /// disable implicit `--locked` flag for all `cargo` commands, enabled by default +pub struct Command { + /// enable `--locked` flag for all `cargo` commands, disabled by default #[interactive_clap(long)] - pub no_locked: bool, + pub locked: bool, /// Include rustdocs in the ABI file #[interactive_clap(long)] pub no_doc: bool, @@ -28,10 +28,10 @@ pub struct AbiCommand { pub color: Option, } -impl From for AbiOpts { - fn from(value: AbiCommand) -> Self { +impl From for AbiOpts { + fn from(value: Command) -> Self { Self { - no_locked: value.no_locked, + no_locked: !value.locked, no_doc: value.no_doc, compact_abi: value.compact_abi, out_dir: value.out_dir.map(Into::into), @@ -47,10 +47,10 @@ pub struct AbiCommandlContext; impl AbiCommandlContext { pub fn from_previous_context( _previous_context: near_cli_rs::GlobalContext, - scope: &::InteractiveClapContextScope, + scope: &::InteractiveClapContextScope, ) -> color_eyre::eyre::Result { - let args = AbiCommand { - no_locked: scope.no_locked, + let args = Command { + locked: scope.locked, no_doc: scope.no_doc, compact_abi: scope.compact_abi, out_dir: scope.out_dir.clone(), diff --git a/cargo-near/src/commands/build/actions/non_reproducible_wasm/mod.rs b/cargo-near/src/commands/build/actions/non_reproducible_wasm/mod.rs new file mode 100644 index 00000000..4338375e --- /dev/null +++ b/cargo-near/src/commands/build/actions/non_reproducible_wasm/mod.rs @@ -0,0 +1,215 @@ +use cargo_near_build::BuildArtifact; + +#[derive(Default, Debug, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(input_context = cargo_near_build::BuildContext)] +#[interactive_clap(output_context = context::Context)] +pub struct BuildOpts { + /// enable `--locked` flag for all `cargo` commands, disabled by default + #[interactive_clap(long)] + pub locked: bool, + /// Build contract in debug mode, without optimizations and bigger is size + #[interactive_clap(long)] + pub no_release: bool, + /// Do not generate ABI for the contract + #[interactive_clap(long)] + pub no_abi: bool, + /// Do not embed the ABI in the contract binary + #[interactive_clap(long)] + pub no_embed_abi: bool, + /// Do not include rustdocs in the embedded ABI + #[interactive_clap(long)] + pub no_doc: bool, + /// Do not run `wasm-opt -O` on the generated output as a post-step + #[interactive_clap(long)] + pub no_wasmopt: bool, + /// Copy final artifacts to this directory + #[interactive_clap(long)] + #[interactive_clap(skip_interactive_input)] + pub out_dir: Option, + /// Path to the `Cargo.toml` of the contract to build + #[interactive_clap(long)] + #[interactive_clap(skip_interactive_input)] + pub manifest_path: Option, + /// Set compile-time feature flags. + #[interactive_clap(long)] + #[interactive_clap(skip_interactive_input)] + pub features: Option, + /// Disables default feature flags. + #[interactive_clap(long)] + #[interactive_clap(skip_interactive_input)] + pub no_default_features: bool, + /// Coloring: auto, always, never + #[interactive_clap(long)] + #[interactive_clap(value_enum)] + #[interactive_clap(skip_interactive_input)] + pub color: Option, + /// env overrides in the form of `"KEY=VALUE"` strings + #[interactive_clap(long_vec_multiple_opt)] + pub env: Vec, +} + +impl From for BuildOpts { + fn from(value: CliBuildOpts) -> Self { + Self { + locked: value.locked, + no_release: value.no_release, + no_abi: value.no_abi, + no_embed_abi: value.no_embed_abi, + no_doc: value.no_doc, + no_wasmopt: value.no_wasmopt, + out_dir: value.out_dir, + manifest_path: value.manifest_path, + features: value.features, + no_default_features: value.no_default_features, + color: value.color, + env: value.env, + } + } +} + +pub mod context { + + #[derive(Debug)] + pub struct Context; + + impl Context { + pub fn from_previous_context( + _previous_context: cargo_near_build::BuildContext, + scope: &::InteractiveClapContextScope, + ) -> color_eyre::eyre::Result { + let opts = super::BuildOpts { + locked: scope.locked, + no_release: scope.no_release, + no_abi: scope.no_abi, + no_embed_abi: scope.no_embed_abi, + no_doc: scope.no_doc, + no_wasmopt: scope.no_wasmopt, + features: scope.features.clone(), + no_default_features: scope.no_default_features, + env: scope.env.clone(), + out_dir: scope.out_dir.clone(), + manifest_path: scope.manifest_path.clone(), + color: scope.color.clone(), + }; + super::run(opts)?; + Ok(Self) + } + } +} + +impl From for cargo_near_build::BuildOpts { + fn from(value: BuildOpts) -> Self { + Self { + no_locked: !value.locked, + no_release: value.no_release, + no_abi: value.no_abi, + no_embed_abi: value.no_embed_abi, + no_doc: value.no_doc, + no_wasmopt: value.no_wasmopt, + features: value.features, + no_default_features: value.no_default_features, + out_dir: value.out_dir.map(Into::into), + manifest_path: value.manifest_path.map(Into::into), + color: value.color.map(Into::into), + cli_description: Default::default(), + env: env_pairs::get_key_vals(value.env), + override_nep330_contract_path: None, + override_cargo_target_dir: None, + } + } +} + +mod env_pairs { + use std::collections::HashMap; + + impl super::BuildOpts { + pub(super) fn validate_env_opt(&self) -> color_eyre::eyre::Result<()> { + for pair in self.env.iter() { + pair.split_once('=').ok_or(color_eyre::eyre::eyre!( + "invalid \"key=value\" environment argument (must contain '='): {}", + pair + ))?; + } + Ok(()) + } + } + + pub(super) fn get_key_vals(input: Vec) -> Vec<(String, String)> { + let iterator = input.iter().flat_map(|pair_string| { + pair_string + .split_once('=') + .map(|(env_key, value)| (env_key.to_string(), value.to_string())) + }); + + let dedup_map: HashMap = HashMap::from_iter(iterator); + + let result = dedup_map.into_iter().collect(); + tracing::info!( + target: "near_teach_me", + parent: &tracing::Span::none(), + "Passed additional environment pairs:\n{}", + near_cli_rs::common::indent_payload(&format!("{:#?}", result)) + ); + result + } +} + +pub mod rule { + use color_eyre::Section; + use colored::Colorize; + + const COMMAND_ERR_MSG: &str = "`container_build_command` is required to start with"; + + fn is_inside_docker_context() -> bool { + std::env::var(cargo_near_build::env_keys::nep330::BUILD_ENVIRONMENT).is_ok() + } + pub fn assert_locked(opts: &super::BuildOpts) { + if is_inside_docker_context() { + assert!( + opts.locked, + "build command should have `--locked` flag in docker" + ); + } + } + + fn get_docker_image() -> String { + std::env::var(cargo_near_build::env_keys::nep330::BUILD_ENVIRONMENT).unwrap_or_else(|_| { + panic!( + "`{}` is expected to be set", + cargo_near_build::env_keys::nep330::BUILD_ENVIRONMENT + ) + }) + } + pub fn enforce_this_program_args() -> color_eyre::eyre::Result<()> { + if is_inside_docker_context() { + let args = std::env::args().collect::>(); + let default_cmd = + cargo_near_build::BuildOpts::default().get_cli_command_for_lib_context(); + let default_cmd_len = default_cmd.len(); + if (args.len() < default_cmd_len) + || (args[1..default_cmd_len] != default_cmd[1..default_cmd_len]) + { + return Err(color_eyre::eyre::eyre!( + "{}\n`{}` for the used image:\n{}", + COMMAND_ERR_MSG, + serde_json::to_string(&default_cmd).unwrap(), + get_docker_image() + ) + .note(format!( + "The default `{}` has changed since `{}` image\n\ + See {}", + "container_build_command".cyan(), + "sourcescan/cargo-near:0.13.0-rust-1.83.0".cyan(), + "https://github.com/near/cargo-near/releases".cyan() + ))); + } + } + Ok(()) + } +} + +pub fn run(opts: BuildOpts) -> color_eyre::eyre::Result { + rule::assert_locked(&opts); + opts.validate_env_opt()?; + cargo_near_build::build(opts.into()) +} diff --git a/cargo-near/src/commands/build/actions/reproducible_wasm/mod.rs b/cargo-near/src/commands/build/actions/reproducible_wasm/mod.rs new file mode 100644 index 00000000..6fad36a0 --- /dev/null +++ b/cargo-near/src/commands/build/actions/reproducible_wasm/mod.rs @@ -0,0 +1,76 @@ +use cargo_near_build::BuildContext; + +use cargo_near_build::docker; +use cargo_near_build::BuildArtifact; + +#[derive(Debug, Default, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(input_context = cargo_near_build::BuildContext)] +#[interactive_clap(output_context = context::Context)] +pub struct BuildOpts { + /// disable implicit `--locked` flag for all `cargo` commands, enabled by default + #[interactive_clap(long)] + pub no_locked: bool, + /// Copy final artifacts to this directory + #[interactive_clap(long)] + #[interactive_clap(skip_interactive_input)] + pub out_dir: Option, + /// Path to the `Cargo.toml` of the contract to build + #[interactive_clap(long)] + #[interactive_clap(skip_interactive_input)] + pub manifest_path: Option, + /// Coloring: auto, always, never + #[interactive_clap(long)] + #[interactive_clap(value_enum)] + #[interactive_clap(skip_interactive_input)] + pub color: Option, +} + +impl From for BuildOpts { + fn from(value: CliBuildOpts) -> Self { + Self { + no_locked: value.no_locked, + out_dir: value.out_dir, + manifest_path: value.manifest_path, + color: value.color, + } + } +} + +mod context { + #[derive(Debug)] + pub struct Context; + + impl Context { + pub fn from_previous_context( + previous_context: cargo_near_build::BuildContext, + scope: &::InteractiveClapContextScope, + ) -> color_eyre::eyre::Result { + let opts = super::BuildOpts { + no_locked: scope.no_locked, + out_dir: scope.out_dir.clone(), + manifest_path: scope.manifest_path.clone(), + color: scope.color.clone(), + }; + super::run(opts, previous_context)?; + Ok(Self) + } + } +} + +/// this is more or less equivalent to +/// impl From<(BuildCommand, BuildContext)> for docker::DockerBuildOpts +/// which is not possible due to BuildContext being a non-local type to current (cli) crate +fn docker_opts_from(value: (BuildOpts, BuildContext)) -> docker::DockerBuildOpts { + docker::DockerBuildOpts { + no_locked: value.0.no_locked, + out_dir: value.0.out_dir.map(Into::into), + manifest_path: value.0.manifest_path.map(Into::into), + color: value.0.color.map(Into::into), + context: value.1, + } +} + +pub fn run(opts: BuildOpts, context: BuildContext) -> color_eyre::eyre::Result { + let docker_opts = docker_opts_from((opts, context)); + cargo_near_build::docker::build(docker_opts) +} diff --git a/cargo-near/src/commands/build/mod.rs b/cargo-near/src/commands/build/mod.rs new file mode 100644 index 00000000..77d98447 --- /dev/null +++ b/cargo-near/src/commands/build/mod.rs @@ -0,0 +1,50 @@ +pub mod context { + #[derive(Debug, Clone)] + pub struct Context; + + impl From for cargo_near_build::BuildContext { + fn from(_value: Context) -> Self { + Self::Build + } + } + + impl Context { + pub fn from_previous_context( + _previous_context: near_cli_rs::GlobalContext, + _scope: &::InteractiveClapContextScope, + ) -> color_eyre::eyre::Result { + Ok(Self) + } + } +} + +pub mod actions { + pub mod non_reproducible_wasm; + pub mod reproducible_wasm; + + use strum::{EnumDiscriminants, EnumIter, EnumMessage}; + + #[derive(Debug, Clone, EnumDiscriminants, interactive_clap::InteractiveClap)] + #[strum_discriminants(derive(EnumMessage, EnumIter))] + #[interactive_clap(input_context = near_cli_rs::GlobalContext)] + #[interactive_clap(output_context = super::context::Context)] + pub enum Actions { + #[strum_discriminants(strum( + message = "non-reproducible-wasm - Fast and simple (recommended for use during local development)" + ))] + /// Fast and simple (recommended for use during local development) + NonReproducibleWasm(self::non_reproducible_wasm::BuildOpts), + #[strum_discriminants(strum( + message = "reproducible-wasm - Requires [reproducible_build] section in Cargo.toml, and all changes committed to git (recommended for the production release)" + ))] + /// Requires `[reproducible_build]` section in Cargo.toml, and all changes committed to git (recommended for the production release) + ReproducibleWasm(self::reproducible_wasm::BuildOpts), + } +} + +#[derive(Debug, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(context = near_cli_rs::GlobalContext)] +pub struct Command { + #[interactive_clap(subcommand)] + actions: actions::Actions, +} diff --git a/cargo-near/src/commands/build_command/mod.rs b/cargo-near/src/commands/build_command/mod.rs deleted file mode 100644 index 5510ce6d..00000000 --- a/cargo-near/src/commands/build_command/mod.rs +++ /dev/null @@ -1,198 +0,0 @@ -use std::collections::HashMap; - -use cargo_near_build::docker; -use cargo_near_build::{env_keys, BuildArtifact, BuildContext, BuildOpts}; - -#[derive(Debug, Default, Clone, interactive_clap::InteractiveClap)] -#[interactive_clap(input_context = near_cli_rs::GlobalContext)] -#[interactive_clap(output_context = BuildCommandlContext)] -pub struct BuildCommand { - /// disable implicit `--locked` flag for all `cargo` commands, enabled by default - #[interactive_clap(long)] - pub no_locked: bool, - /// Build contract on host system and without embedding SourceScan NEP-330 metadata - #[interactive_clap(long)] - no_docker: bool, - /// Build contract in debug mode, without optimizations and bigger is size - #[interactive_clap(long)] - pub no_release: bool, - /// Do not generate ABI for the contract - #[interactive_clap(long)] - pub no_abi: bool, - /// Do not embed the ABI in the contract binary - #[interactive_clap(long)] - pub no_embed_abi: bool, - /// Do not include rustdocs in the embedded ABI - #[interactive_clap(long)] - pub no_doc: bool, - /// Do not run `wasm-opt -O` on the generated output as a post-step - #[interactive_clap(long)] - pub no_wasmopt: bool, - /// Copy final artifacts to this directory - #[interactive_clap(long)] - #[interactive_clap(skip_interactive_input)] - pub out_dir: Option, - /// Path to the `Cargo.toml` of the contract to build - #[interactive_clap(long)] - #[interactive_clap(skip_interactive_input)] - pub manifest_path: Option, - /// Set compile-time feature flags. - #[interactive_clap(long)] - #[interactive_clap(skip_interactive_input)] - pub features: Option, - /// Disables default feature flags. - #[interactive_clap(long)] - #[interactive_clap(skip_interactive_input)] - pub no_default_features: bool, - /// Coloring: auto, always, never - #[interactive_clap(long)] - #[interactive_clap(value_enum)] - #[interactive_clap(skip_interactive_input)] - pub color: Option, - /// env overrides in the form of `"KEY=VALUE"` strings - #[interactive_clap(long_vec_multiple_opt)] - pub env: Vec, -} - -impl BuildCommand { - fn validate_env_opt(&self) -> color_eyre::eyre::Result<()> { - for pair in self.env.iter() { - pair.split_once('=').ok_or(color_eyre::eyre::eyre!( - "invalid \"key=value\" environment argument (must contain '='): {}", - pair - ))?; - } - Ok(()) - } - pub fn run(self, context: BuildContext) -> color_eyre::eyre::Result { - self.validate_env_opt()?; - if self.no_docker() { - if let BuildContext::Deploy { - skip_git_remote_check: true, - } = context - { - return Err(color_eyre::eyre::eyre!( - "`--skip-git-remote-check` flag is only applicable for docker builds" - )); - } - cargo_near_build::build(self.into()) - } else { - let docker_opts = docker_opts_from((self, context)); - cargo_near_build::docker::build(docker_opts) - } - } - pub fn no_docker(&self) -> bool { - std::env::var(env_keys::nep330::BUILD_ENVIRONMENT).is_ok() || self.no_docker - } -} - -impl From for BuildCommand { - fn from(value: CliBuildCommand) -> Self { - Self { - no_locked: value.no_locked, - 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, - no_wasmopt: value.no_wasmopt, - features: value.features, - no_default_features: value.no_default_features, - out_dir: value.out_dir, - manifest_path: value.manifest_path, - color: value.color, - env: value.env, - } - } -} - -fn get_env_key_vals(input: Vec) -> Vec<(String, String)> { - let iterator = input.iter().flat_map(|pair_string| { - pair_string - .split_once('=') - .map(|(env_key, value)| (env_key.to_string(), value.to_string())) - }); - - let dedup_map: HashMap = HashMap::from_iter(iterator); - - let result = dedup_map.into_iter().collect(); - tracing::info!( - target: "near_teach_me", - parent: &tracing::Span::none(), - "Passed additional environment pairs:\n{}", - near_cli_rs::common::indent_payload(&format!("{:#?}", result)) - ); - result -} - -impl From for BuildOpts { - fn from(value: BuildCommand) -> Self { - Self { - no_locked: value.no_locked, - no_release: value.no_release, - no_abi: value.no_abi, - no_embed_abi: value.no_embed_abi, - no_doc: value.no_doc, - no_wasmopt: value.no_wasmopt, - features: value.features, - no_default_features: value.no_default_features, - out_dir: value.out_dir.map(Into::into), - manifest_path: value.manifest_path.map(Into::into), - color: value.color.map(Into::into), - cli_description: Default::default(), - env: get_env_key_vals(value.env), - override_nep330_contract_path: None, - override_cargo_target_dir: None, - } - } -} - -/// this is more or less equivalent to -/// impl From<(BuildCommand, BuildContext)> for docker::DockerBuildOpts -/// which is not possible due to BuildContext being a non-local type to current (cli) crate -fn docker_opts_from(value: (BuildCommand, BuildContext)) -> docker::DockerBuildOpts { - docker::DockerBuildOpts { - no_locked: value.0.no_locked, - no_release: value.0.no_release, - no_abi: value.0.no_abi, - no_embed_abi: value.0.no_embed_abi, - no_doc: value.0.no_doc, - no_wasmopt: value.0.no_wasmopt, - features: value.0.features, - no_default_features: value.0.no_default_features, - out_dir: value.0.out_dir.map(Into::into), - manifest_path: value.0.manifest_path.map(Into::into), - color: value.0.color.map(Into::into), - cli_description: Default::default(), - env: get_env_key_vals(value.0.env), - context: value.1, - } -} - -#[derive(Debug, Clone)] -pub struct BuildCommandlContext; - -impl BuildCommandlContext { - pub fn from_previous_context( - _previous_context: near_cli_rs::GlobalContext, - scope: &::InteractiveClapContextScope, - ) -> color_eyre::eyre::Result { - let args = BuildCommand { - no_locked: scope.no_locked, - no_docker: scope.no_docker, - no_release: scope.no_release, - no_abi: scope.no_abi, - no_embed_abi: scope.no_embed_abi, - no_doc: scope.no_doc, - no_wasmopt: scope.no_wasmopt, - out_dir: scope.out_dir.clone(), - manifest_path: scope.manifest_path.clone(), - features: scope.features.clone(), - no_default_features: scope.no_default_features, - color: scope.color.clone(), - env: scope.env.clone(), - }; - args.run(BuildContext::Build)?; - Ok(Self) - } -} diff --git a/cargo-near/src/commands/deploy/actions/non_reproducible_wasm.rs b/cargo-near/src/commands/deploy/actions/non_reproducible_wasm.rs new file mode 100644 index 00000000..8bfab79d --- /dev/null +++ b/cargo-near/src/commands/deploy/actions/non_reproducible_wasm.rs @@ -0,0 +1,149 @@ +use near_cli_rs::commands::contract::deploy::initialize_mode::InitializeMode; + +use crate::commands::build as build_command; + +#[derive(Debug, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(input_context = near_cli_rs::GlobalContext)] +#[interactive_clap(output_context = context::Context)] +#[interactive_clap(skip_default_from_cli)] +pub struct DeployOpts { + #[interactive_clap(flatten)] + /// Specify a build command args: + build_command_opts: build_command::actions::non_reproducible_wasm::BuildOpts, + #[interactive_clap(skip_default_input_arg)] + /// What is the contract account ID? + contract_account_id: near_cli_rs::types::account_id::AccountId, + #[interactive_clap(subcommand)] + initialize: InitializeMode, +} + +mod context { + use crate::commands::build as build_command; + + #[derive(Debug, Clone)] + pub struct Context(near_cli_rs::commands::contract::deploy::ContractFileContext); + + impl From for near_cli_rs::commands::contract::deploy::ContractFileContext { + fn from(item: Context) -> Self { + item.0 + } + } + + impl Context { + pub fn from_previous_context( + previous_context: near_cli_rs::GlobalContext, + scope: &::InteractiveClapContextScope, + ) -> color_eyre::eyre::Result { + let artifact = build_command::actions::non_reproducible_wasm::run( + scope.build_command_opts.clone(), + )?; + + let code = std::fs::read(&artifact.path).map_err(|err| { + color_eyre::eyre::eyre!( + "accessing {} to read wasm contents: {}", + artifact.path, + err + ) + })?; + Ok(Self( + near_cli_rs::commands::contract::deploy::ContractFileContext { + global_context: previous_context, + receiver_account_id: scope.contract_account_id.clone().into(), + signer_account_id: scope.contract_account_id.clone().into(), + code, + }, + )) + } + } +} + +/// this module is needed because of `#[interactive_clap(skip_default_input_arg)]` +/// on `contract_account_id` +mod manual_input { + impl super::DeployOpts { + pub fn input_contract_account_id( + context: &near_cli_rs::GlobalContext, + ) -> color_eyre::eyre::Result> { + near_cli_rs::common::input_signer_account_id_from_used_account_list( + &context.config.credentials_home_dir, + "What is the contract account ID?", + ) + } + } +} + +/// this module is needed because of #[interactive_clap(skip_default_from_cli)] +/// on `DeployOpts` +mod manual_from_cli { + use crate::commands::build as build_command; + use near_cli_rs::commands::contract::deploy::initialize_mode::InitializeMode; + + impl interactive_clap::FromCli for super::DeployOpts { + type FromCliContext = near_cli_rs::GlobalContext; + type FromCliError = color_eyre::eyre::Error; + fn from_cli( + optional_clap_variant: Option<::CliVariant>, + context: Self::FromCliContext, + ) -> interactive_clap::ResultFromCli< + ::CliVariant, + Self::FromCliError, + > + where + Self: Sized + interactive_clap::ToCli, + { + let mut clap_variant = optional_clap_variant.unwrap_or_default(); + + let build_command_opts = + if let Some(cli_build_command_opts) = &clap_variant.build_command_opts { + build_command::actions::non_reproducible_wasm::BuildOpts::from( + cli_build_command_opts.clone(), + ) + } else { + build_command::actions::non_reproducible_wasm::BuildOpts::default() + }; + + if clap_variant.contract_account_id.is_none() { + clap_variant.contract_account_id = match Self::input_contract_account_id(&context) { + Ok(Some(contract_account_id)) => Some(contract_account_id), + Ok(None) => return interactive_clap::ResultFromCli::Cancel(Some(clap_variant)), + Err(err) => { + return interactive_clap::ResultFromCli::Err(Some(clap_variant), err) + } + }; + } + let contract_account_id = clap_variant + .contract_account_id + .clone() + .expect("Unexpected error"); + + let new_context_scope = super::InteractiveClapContextScopeForDeployOpts { + build_command_opts, + contract_account_id, + }; + + let output_context = + match super::context::Context::from_previous_context(context, &new_context_scope) { + Ok(new_context) => new_context, + Err(err) => { + return interactive_clap::ResultFromCli::Err(Some(clap_variant), err) + } + }; + + match InitializeMode::from_cli(clap_variant.initialize.take(), output_context.into()) { + interactive_clap::ResultFromCli::Ok(initialize) => { + clap_variant.initialize = Some(initialize); + interactive_clap::ResultFromCli::Ok(clap_variant) + } + interactive_clap::ResultFromCli::Cancel(optional_initialize) => { + clap_variant.initialize = optional_initialize; + interactive_clap::ResultFromCli::Cancel(Some(clap_variant)) + } + interactive_clap::ResultFromCli::Back => interactive_clap::ResultFromCli::Back, + interactive_clap::ResultFromCli::Err(optional_initialize, err) => { + clap_variant.initialize = optional_initialize; + interactive_clap::ResultFromCli::Err(Some(clap_variant), err) + } + } + } + } +} diff --git a/cargo-near/src/commands/deploy/actions/reproducible_wasm.rs b/cargo-near/src/commands/deploy/actions/reproducible_wasm.rs new file mode 100644 index 00000000..5c4ff3be --- /dev/null +++ b/cargo-near/src/commands/deploy/actions/reproducible_wasm.rs @@ -0,0 +1,158 @@ +use near_cli_rs::commands::contract::deploy::initialize_mode::InitializeMode; + +use crate::commands::build as build_command; + +#[derive(Debug, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(input_context = near_cli_rs::GlobalContext)] +#[interactive_clap(output_context = context::Context)] +#[interactive_clap(skip_default_from_cli)] +pub struct DeployOpts { + #[interactive_clap(flatten)] + /// Specify a build command args: + build_command_opts: build_command::actions::reproducible_wasm::BuildOpts, + /// whether to check that code has been pushed to repository during docker build + #[interactive_clap(long)] + skip_git_remote_check: bool, + #[interactive_clap(skip_default_input_arg)] + /// What is the contract account ID? + contract_account_id: near_cli_rs::types::account_id::AccountId, + #[interactive_clap(subcommand)] + initialize: InitializeMode, +} + +mod context { + use crate::commands::build as build_command; + + #[derive(Debug, Clone)] + pub struct Context(near_cli_rs::commands::contract::deploy::ContractFileContext); + + impl From for near_cli_rs::commands::contract::deploy::ContractFileContext { + fn from(item: Context) -> Self { + item.0 + } + } + + impl Context { + pub fn from_previous_context( + previous_context: near_cli_rs::GlobalContext, + scope: &::InteractiveClapContextScope, + ) -> color_eyre::eyre::Result { + let artifact = build_command::actions::reproducible_wasm::run( + scope.build_command_opts.clone(), + cargo_near_build::BuildContext::Deploy { + skip_git_remote_check: scope.skip_git_remote_check, + }, + )?; + let code = std::fs::read(&artifact.path).map_err(|err| { + color_eyre::eyre::eyre!( + "accessing {} to read wasm contents: {}", + artifact.path, + err + ) + })?; + + Ok(Self( + near_cli_rs::commands::contract::deploy::ContractFileContext { + global_context: previous_context, + receiver_account_id: scope.contract_account_id.clone().into(), + signer_account_id: scope.contract_account_id.clone().into(), + code, + }, + )) + } + } +} + +/// this module is needed because of `#[interactive_clap(skip_default_input_arg)]` +/// on `contract_account_id` +mod manual_input { + impl super::DeployOpts { + pub fn input_contract_account_id( + context: &near_cli_rs::GlobalContext, + ) -> color_eyre::eyre::Result> { + near_cli_rs::common::input_signer_account_id_from_used_account_list( + &context.config.credentials_home_dir, + "What is the contract account ID?", + ) + } + } +} + +/// this module is needed because of #[interactive_clap(skip_default_from_cli)] +/// on `DeployOpts` +mod manual_from_cli { + use crate::commands::build as build_command; + use near_cli_rs::commands::contract::deploy::initialize_mode::InitializeMode; + + impl interactive_clap::FromCli for super::DeployOpts { + type FromCliContext = near_cli_rs::GlobalContext; + type FromCliError = color_eyre::eyre::Error; + fn from_cli( + optional_clap_variant: Option<::CliVariant>, + context: Self::FromCliContext, + ) -> interactive_clap::ResultFromCli< + ::CliVariant, + Self::FromCliError, + > + where + Self: Sized + interactive_clap::ToCli, + { + let mut clap_variant = optional_clap_variant.unwrap_or_default(); + + let build_command_opts = + if let Some(cli_build_command_opts) = &clap_variant.build_command_opts { + build_command::actions::reproducible_wasm::BuildOpts::from( + cli_build_command_opts.clone(), + ) + } else { + build_command::actions::reproducible_wasm::BuildOpts::default() + }; + + if clap_variant.contract_account_id.is_none() { + clap_variant.contract_account_id = match Self::input_contract_account_id(&context) { + Ok(Some(contract_account_id)) => Some(contract_account_id), + Ok(None) => return interactive_clap::ResultFromCli::Cancel(Some(clap_variant)), + Err(err) => { + return interactive_clap::ResultFromCli::Err(Some(clap_variant), err) + } + }; + } + let contract_account_id = clap_variant + .contract_account_id + .clone() + .expect("Unexpected error"); + + let skip_git_remote_check = clap_variant.skip_git_remote_check; + + let new_context_scope = super::InteractiveClapContextScopeForDeployOpts { + build_command_opts, + contract_account_id, + skip_git_remote_check, + }; + + let output_context = + match super::context::Context::from_previous_context(context, &new_context_scope) { + Ok(new_context) => new_context, + Err(err) => { + return interactive_clap::ResultFromCli::Err(Some(clap_variant), err) + } + }; + + match InitializeMode::from_cli(clap_variant.initialize.take(), output_context.into()) { + interactive_clap::ResultFromCli::Ok(initialize) => { + clap_variant.initialize = Some(initialize); + interactive_clap::ResultFromCli::Ok(clap_variant) + } + interactive_clap::ResultFromCli::Cancel(optional_initialize) => { + clap_variant.initialize = optional_initialize; + interactive_clap::ResultFromCli::Cancel(Some(clap_variant)) + } + interactive_clap::ResultFromCli::Back => interactive_clap::ResultFromCli::Back, + interactive_clap::ResultFromCli::Err(optional_initialize, err) => { + clap_variant.initialize = optional_initialize; + interactive_clap::ResultFromCli::Err(Some(clap_variant), err) + } + } + } + } +} diff --git a/cargo-near/src/commands/deploy/mod.rs b/cargo-near/src/commands/deploy/mod.rs index a3e81c15..c462cf32 100644 --- a/cargo-near/src/commands/deploy/mod.rs +++ b/cargo-near/src/commands/deploy/mod.rs @@ -1,131 +1,29 @@ -use near_cli_rs::commands::contract::deploy::initialize_mode::InitializeMode; - -use crate::commands::build_command; - -#[derive(Debug, Clone, interactive_clap::InteractiveClap)] -#[interactive_clap(input_context = near_cli_rs::GlobalContext)] -#[interactive_clap(output_context = ContractContext)] -#[interactive_clap(skip_default_from_cli)] -pub struct Contract { - #[interactive_clap(flatten)] - /// Specify a build command args: - build_command_args: build_command::BuildCommand, - /// whether to check that code has been pushed to repository during docker build - #[interactive_clap(long)] - skip_git_remote_check: bool, - #[interactive_clap(skip_default_input_arg)] - /// What is the contract account ID? - contract_account_id: near_cli_rs::types::account_id::AccountId, - #[interactive_clap(subcommand)] - initialize: InitializeMode, -} - -#[derive(Debug, Clone)] -pub struct ContractContext(near_cli_rs::commands::contract::deploy::ContractFileContext); - -impl ContractContext { - pub fn from_previous_context( - previous_context: near_cli_rs::GlobalContext, - scope: &::InteractiveClapContextScope, - ) -> color_eyre::eyre::Result { - let file_path = scope - .build_command_args - .clone() - .run(cargo_near_build::BuildContext::Deploy { - skip_git_remote_check: scope.skip_git_remote_check, - })? - .path; - - Ok(Self( - near_cli_rs::commands::contract::deploy::ContractFileContext { - global_context: previous_context, - receiver_account_id: scope.contract_account_id.clone().into(), - signer_account_id: scope.contract_account_id.clone().into(), - code: std::fs::read(file_path)?, - }, - )) - } -} - -impl From for near_cli_rs::commands::contract::deploy::ContractFileContext { - fn from(item: ContractContext) -> Self { - item.0 +mod actions { + mod non_reproducible_wasm; + mod reproducible_wasm; + + use strum::{EnumDiscriminants, EnumIter, EnumMessage}; + + #[derive(Debug, Clone, EnumDiscriminants, interactive_clap::InteractiveClap)] + #[strum_discriminants(derive(EnumMessage, EnumIter))] + #[interactive_clap(context = near_cli_rs::GlobalContext)] + pub enum Actions { + #[strum_discriminants(strum( + message = "build-non-reproducible-wasm - Fast and simple build (recommended for use during local development)" + ))] + /// Fast and simple build (recommended for use during local development) + BuildNonReproducibleWasm(self::non_reproducible_wasm::DeployOpts), + #[strum_discriminants(strum( + message = "build-reproducible-wasm - Build requires [reproducible_build] section in Cargo.toml, and all changes committed and pushed to git (recommended for the production release)" + ))] + /// Build requires `[reproducible_build]` section in Cargo.toml, and all changes committed and pushed to git (recommended for the production release) + BuildReproducibleWasm(self::reproducible_wasm::DeployOpts), } } -impl interactive_clap::FromCli for Contract { - type FromCliContext = near_cli_rs::GlobalContext; - type FromCliError = color_eyre::eyre::Error; - fn from_cli( - optional_clap_variant: Option<::CliVariant>, - context: Self::FromCliContext, - ) -> interactive_clap::ResultFromCli< - ::CliVariant, - Self::FromCliError, - > - where - Self: Sized + interactive_clap::ToCli, - { - let mut clap_variant = optional_clap_variant.unwrap_or_default(); - - let build_command_args = - if let Some(cli_build_command_args) = &clap_variant.build_command_args { - build_command::BuildCommand::from(cli_build_command_args.clone()) - } else { - build_command::BuildCommand::default() - }; - - if clap_variant.contract_account_id.is_none() { - clap_variant.contract_account_id = match Self::input_contract_account_id(&context) { - Ok(Some(contract_account_id)) => Some(contract_account_id), - Ok(None) => return interactive_clap::ResultFromCli::Cancel(Some(clap_variant)), - Err(err) => return interactive_clap::ResultFromCli::Err(Some(clap_variant), err), - }; - } - let contract_account_id = clap_variant - .contract_account_id - .clone() - .expect("Unexpected error"); - - let skip_git_remote_check = clap_variant.skip_git_remote_check; - - let new_context_scope = InteractiveClapContextScopeForContract { - build_command_args, - contract_account_id, - skip_git_remote_check, - }; - - let output_context = - match ContractContext::from_previous_context(context, &new_context_scope) { - Ok(new_context) => new_context, - Err(err) => return interactive_clap::ResultFromCli::Err(Some(clap_variant), err), - }; - - match InitializeMode::from_cli(clap_variant.initialize.take(), output_context.into()) { - interactive_clap::ResultFromCli::Ok(initialize) => { - clap_variant.initialize = Some(initialize); - interactive_clap::ResultFromCli::Ok(clap_variant) - } - interactive_clap::ResultFromCli::Cancel(optional_initialize) => { - clap_variant.initialize = optional_initialize; - interactive_clap::ResultFromCli::Cancel(Some(clap_variant)) - } - interactive_clap::ResultFromCli::Back => interactive_clap::ResultFromCli::Back, - interactive_clap::ResultFromCli::Err(optional_initialize, err) => { - clap_variant.initialize = optional_initialize; - interactive_clap::ResultFromCli::Err(Some(clap_variant), err) - } - } - } -} - -impl Contract { - pub fn input_contract_account_id( - context: &near_cli_rs::GlobalContext, - ) -> color_eyre::eyre::Result> { - near_cli_rs::common::input_signer_account_id_from_used_account_list( - &context.config.credentials_home_dir, - "What is the contract account ID?", - ) - } +#[derive(Debug, Clone, interactive_clap::InteractiveClap)] +#[interactive_clap(context = near_cli_rs::GlobalContext)] +pub struct Command { + #[interactive_clap(subcommand)] + actions: actions::Actions, } diff --git a/cargo-near/src/commands/mod.rs b/cargo-near/src/commands/mod.rs index 37788456..b5c8a081 100644 --- a/cargo-near/src/commands/mod.rs +++ b/cargo-near/src/commands/mod.rs @@ -1,7 +1,7 @@ use strum::{EnumDiscriminants, EnumIter, EnumMessage}; -pub mod abi_command; -pub mod build_command; +pub mod abi; +pub mod build; pub mod create_dev_account; pub mod deploy; pub mod new; @@ -19,15 +19,15 @@ pub enum NearCommand { /// Initializes a new project to create a contract New(self::new::New), #[strum_discriminants(strum( - message = "build - Build a NEAR contract with embed ABI (opt out by passing `--no-embed-abi`)" + message = "build - Build a NEAR contract with embedded ABI" ))] - /// Build a NEAR contract with embed ABI (opt out by passing `--no-embed-abi`) - Build(self::build_command::BuildCommand), + /// Build a NEAR contract with embedded ABI + Build(self::build::Command), #[strum_discriminants(strum( message = "abi - Generates ABI for the contract" ))] /// Generates ABI for the contract - Abi(self::abi_command::AbiCommand), + Abi(self::abi::Command), #[strum_discriminants(strum( message = "create-dev-account - Create a development account using a faucet service sponsor to receive some NEAR tokens (testnet only). To create an account on a different network, use NEAR CLI [https://near.cli.rs]" @@ -37,5 +37,5 @@ pub enum NearCommand { CreateDevAccount(self::create_dev_account::CreateAccount), #[strum_discriminants(strum(message = "deploy - Add a new contract code"))] /// Add a new contract code - Deploy(self::deploy::Contract), + Deploy(self::deploy::Command), } diff --git a/cargo-near/src/commands/new/mod.rs b/cargo-near/src/commands/new/mod.rs index 541b1385..8ceb3943 100644 --- a/cargo-near/src/commands/new/mod.rs +++ b/cargo-near/src/commands/new/mod.rs @@ -210,7 +210,7 @@ const NEW_PROJECT_FILES: &[NewProjectFile] = &[ }, NewProjectFile { file_path: "Cargo.toml", - content: include_str!("new-project-template/Cargo.toml.template"), + content: include_str!("new-project-template/Cargo.template.toml"), }, NewProjectFile { file_path: "README.md", diff --git a/cargo-near/src/commands/new/new-project-template/.github/workflows/deploy-production.yml b/cargo-near/src/commands/new/new-project-template/.github/workflows/deploy-production.yml index f892d532..c3f31aa2 100644 --- a/cargo-near/src/commands/new/new-project-template/.github/workflows/deploy-production.yml +++ b/cargo-near/src/commands/new/new-project-template/.github/workflows/deploy-production.yml @@ -18,7 +18,7 @@ jobs: run: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/near/cargo-near/releases/download/cargo-near-vcargo-near-new-ci-tool-version-self/cargo-near-installer.sh | sh - name: Deploy to production run: | - cargo near deploy "${{ vars.NEAR_CONTRACT_PRODUCTION_ACCOUNT_ID }}" \ + cargo near deploy build-reproducible-wasm "${{ vars.NEAR_CONTRACT_PRODUCTION_ACCOUNT_ID }}" \ without-init-call \ network-config "${{ vars.NEAR_CONTRACT_PRODUCTION_NETWORK }}" \ sign-with-plaintext-private-key \ diff --git a/cargo-near/src/commands/new/new-project-template/.github/workflows/deploy-staging.yml b/cargo-near/src/commands/new/new-project-template/.github/workflows/deploy-staging.yml index 0f75164a..62c781bf 100644 --- a/cargo-near/src/commands/new/new-project-template/.github/workflows/deploy-staging.yml +++ b/cargo-near/src/commands/new/new-project-template/.github/workflows/deploy-staging.yml @@ -41,7 +41,7 @@ jobs: # # WASM reproducibility check akin to SourceScan won't be available for staging contracts, deployed from PRs run: | - cargo near deploy --skip-git-remote-check "${{ env.NEAR_CONTRACT_PR_STAGING_ACCOUNT_ID }}" \ + cargo near deploy build-reproducible-wasm --skip-git-remote-check "${{ env.NEAR_CONTRACT_PR_STAGING_ACCOUNT_ID }}" \ without-init-call \ network-config "${{ vars.NEAR_CONTRACT_STAGING_NETWORK }}" \ sign-with-plaintext-private-key \ diff --git a/cargo-near/src/commands/new/new-project-template/Cargo.toml.template b/cargo-near/src/commands/new/new-project-template/Cargo.template.toml similarity index 100% rename from cargo-near/src/commands/new/new-project-template/Cargo.toml.template rename to cargo-near/src/commands/new/new-project-template/Cargo.template.toml diff --git a/cargo-near/src/commands/new/new-project-template/README.md b/cargo-near/src/commands/new/new-project-template/README.md index 3843c83f..82648b96 100644 --- a/cargo-near/src/commands/new/new-project-template/README.md +++ b/cargo-near/src/commands/new/new-project-template/README.md @@ -22,7 +22,7 @@ Deployment is automated with GitHub Actions CI/CD pipeline. To deploy manually, install [`cargo-near`](https://github.com/near/cargo-near) and run: ```bash -cargo near deploy +cargo near deploy build-reproducible-wasm ``` ## Useful Links diff --git a/cargo-near/src/lib.rs b/cargo-near/src/lib.rs index 17926838..5cf562aa 100644 --- a/cargo-near/src/lib.rs +++ b/cargo-near/src/lib.rs @@ -23,7 +23,7 @@ pub struct Cmd { /// Near pub enum Opts { #[strum_discriminants(strum(message = "near"))] - /// Which cargo extension do you want to use? + /// Which subcommand of `near` extension do you want to use? Near(NearArgs), } diff --git a/cargo-near/src/main.rs b/cargo-near/src/main.rs index 9c0579cc..e82d29a2 100644 --- a/cargo-near/src/main.rs +++ b/cargo-near/src/main.rs @@ -6,9 +6,33 @@ use interactive_clap::ToCliArgs; pub use near_cli_rs::CliResult; -use cargo_near::{setup_tracing, CliOpts, Cmd, Opts}; +use cargo_near::{ + commands::build::actions::non_reproducible_wasm as build_non_reproducible_wasm, setup_tracing, + CliOpts, Cmd, Opts, +}; + +/// this part of cli setup doesn't depend on command arguments in any way +fn pre_setup() -> CliResult { + match env::var("NO_COLOR") { + Ok(v) if v != "0" => colored::control::set_override(false), + _ => colored::control::set_override(std::io::stderr().is_terminal()), + } + + #[cfg(not(debug_assertions))] + let display_env_section = false; + #[cfg(debug_assertions)] + let display_env_section = true; + color_eyre::config::HookBuilder::default() + .display_env_section(display_env_section) + .install()?; + Ok(()) +} fn main() -> CliResult { + pre_setup()?; + + build_non_reproducible_wasm::rule::enforce_this_program_args()?; + let cli_cmd = match Cmd::try_parse() { Ok(cli) => cli, Err(error) => error.exit(), @@ -32,19 +56,6 @@ fn main() -> CliResult { setup_tracing(env::var("RUST_LOG").is_ok(), cli_near_args.teach_me)?; - match env::var("NO_COLOR") { - Ok(v) if v != "0" => colored::control::set_override(false), - _ => colored::control::set_override(std::io::stderr().is_terminal()), - } - - #[cfg(not(debug_assertions))] - let display_env_section = false; - #[cfg(debug_assertions)] - let display_env_section = true; - color_eyre::config::HookBuilder::default() - .display_env_section(display_env_section) - .install()?; - let console_command_path = if env::var("CARGO_HOME").is_ok() { "cargo".to_string() } else if let Ok(value) = env::var("CARGO") { diff --git a/docs/image_and_digest_pinpoint.png b/docs/image_and_digest_pinpoint.png new file mode 100644 index 0000000000000000000000000000000000000000..bd6205659231315d69a2ccb9c73408b5a522c33a GIT binary patch literal 31223 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ᶈkG@S&n*jjMzx4mmn-4NaqRvKp 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 literal 0 HcmV?d00001 diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index f96694c8..f2af844f 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -6,11 +6,13 @@ publish = false [dependencies] const_format = "0.2" +color-eyre = "0.6" cargo-near-build = { version = "0.3.2", path = "../cargo-near-build" } cargo-near = { path = "../cargo-near" } colored = "2.0" tracing = "0.1.40" -tracing-subscriber = { version = "0.3", features = ["env-filter"] } +prettyplease = "0.2" +syn = "2" [dev-dependencies] borsh = { version = "1.0.0", features = ["derive", "unstable__schema"] } diff --git a/integration-tests/src/lib.rs b/integration-tests/src/lib.rs index d3ab5d34..4b65405b 100644 --- a/integration-tests/src/lib.rs +++ b/integration-tests/src/lib.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use cargo_near_build::camino; /// NOTE: `near-sdk` version, published on crates.io @@ -41,81 +43,126 @@ pub fn common_root_for_test_projects_build() -> camino::Utf8PathBuf { workspace_dir } -#[macro_export] -macro_rules! invoke_cargo_near { - ($(Cargo: $cargo_path:expr;)? $(Vars: $cargo_vars:expr;)? Opts: $cli_opts:expr; Code: $($code:tt)*) => {{ - let workspace_dir = $crate::common_root_for_test_projects_build(); - let crate_dir = workspace_dir.join(function_name!()); - let src_dir = crate_dir.join("src"); - std::fs::create_dir_all(&src_dir)?; - - let mut cargo_toml = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/_Cargo.toml")).to_string(); - $(cargo_toml = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), $cargo_path)).to_string())?; - let mut cargo_vars = std::collections::HashMap::new(); - $(cargo_vars = $cargo_vars)?; - cargo_vars.insert("sdk-cratesio-version", $crate::from_crates_io::SDK_VERSION); - cargo_vars.insert("sdk-cratesio-version-toml", $crate::from_crates_io::SDK_VERSION_TOML); - cargo_vars.insert("sdk-git-version", $crate::from_git::SDK_VERSION); - cargo_vars.insert("sdk-git-short-version-toml", $crate::from_git::SDK_SHORT_VERSION_TOML); - cargo_vars.insert("sdk-git-version-toml", $crate::from_git::SDK_VERSION_TOML); - cargo_vars.insert("sdk-git-version-toml-table", $crate::from_git::SDK_VERSION_TOML_TABLE); - cargo_vars.insert("name", function_name!()); - for (k, v) in cargo_vars { - cargo_toml = cargo_toml.replace(&format!("::{}::", k), v); +pub fn invoke_cargo_near( + function_name: &str, + cargo_path: Option<&str>, + mut cargo_vars: HashMap<&str, String>, + lib_rs_file: syn::File, + cli_opts: String, +) -> color_eyre::eyre::Result { + let workspace_dir = crate::common_root_for_test_projects_build(); + let crate_dir = workspace_dir.join(function_name); + let src_dir = crate_dir.join("src"); + std::fs::create_dir_all(&src_dir)?; + + let mut cargo_toml = match cargo_path { + Some(cargo_path) => { + let file = [env!("CARGO_MANIFEST_DIR"), cargo_path].concat(); + String::from_utf8(std::fs::read(&file)?)? } - let cargo_path = crate_dir.join("Cargo.toml"); - std::fs::write(&cargo_path, cargo_toml)?; + None => include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/templates/_Cargo.toml" + )) + .to_string(), + }; + cargo_vars.insert( + "sdk-cratesio-version", + crate::from_crates_io::SDK_VERSION.into(), + ); + cargo_vars.insert( + "sdk-cratesio-version-toml", + crate::from_crates_io::SDK_VERSION_TOML.into(), + ); + cargo_vars.insert("sdk-git-version", crate::from_git::SDK_VERSION.into()); + cargo_vars.insert( + "sdk-git-short-version-toml", + crate::from_git::SDK_SHORT_VERSION_TOML.into(), + ); + cargo_vars.insert( + "sdk-git-version-toml", + crate::from_git::SDK_VERSION_TOML.into(), + ); + cargo_vars.insert( + "sdk-git-version-toml-table", + crate::from_git::SDK_VERSION_TOML_TABLE.into(), + ); + cargo_vars.insert("name", function_name.into()); + for (k, v) in cargo_vars { + cargo_toml = cargo_toml.replace(&format!("::{}::", k), &v); + } + let cargo_path = crate_dir.join("Cargo.toml"); + std::fs::write(&cargo_path, cargo_toml)?; - let lib_rs_file = syn::parse_file("e::quote! { $($code)* }.to_string()).unwrap(); - let lib_rs = prettyplease::unparse(&lib_rs_file); - let lib_rs_path = src_dir.join("lib.rs"); - std::fs::write(lib_rs_path, lib_rs)?; - - let cargo_near::CliOpts::Near(cli_args) = cargo_near::Opts::try_parse_from($cli_opts.split(" "))?; - - let path: camino::Utf8PathBuf = match cli_args.cmd { - Some(cargo_near::commands::CliNearCommand::Abi(cmd)) => { - let args = cargo_near_build::abi::AbiOpts { - no_locked: cmd.no_locked, - no_doc: cmd.no_doc, - compact_abi: cmd.compact_abi, - out_dir: cmd.out_dir.map(Into::into), - manifest_path: Some(cargo_path.into()), - color: cmd.color.map(Into::into), - }; - tracing::debug!("AbiOpts: {:#?}", args); - let path = cargo_near_build::abi::build(args)?; - path - }, - Some(cargo_near::commands::CliNearCommand::Build(cmd)) => { - let args = { - let mut args = cargo_near::commands::build_command::BuildCommand::from(cmd) ; - args.manifest_path = Some(cargo_path.into()); - args - }; - tracing::debug!("BuildCommand: {:#?}", args); - let artifact = args.run(cargo_near_build::BuildContext::Build)?; - artifact.path + let lib_rs = prettyplease::unparse(&lib_rs_file); + let lib_rs_path = src_dir.join("lib.rs"); + std::fs::write(lib_rs_path, lib_rs)?; + + let cargo_near::CliOpts::Near(cli_args) = + cargo_near::Opts::try_parse_from(cli_opts.split(" "))?; + + let path: camino::Utf8PathBuf = match cli_args.cmd { + Some(cargo_near::commands::CliNearCommand::Abi(cmd)) => { + let args = cargo_near_build::abi::AbiOpts { + no_locked: !cmd.locked, + no_doc: cmd.no_doc, + compact_abi: cmd.compact_abi, + out_dir: cmd.out_dir.map(Into::into), + manifest_path: Some(cargo_path), + color: cmd.color.map(Into::into), + }; + tracing::debug!("AbiOpts: {:#?}", args); + cargo_near_build::abi::build(args)? + } + Some(cargo_near::commands::CliNearCommand::Build( + cargo_near::commands::build::CliCommand { + actions: + Some(cargo_near::commands::build::actions::CliActions::NonReproducibleWasm( + cli_build_otps, + )), }, - Some(_) => todo!(), - None => unreachable!(), - }; - path + )) => { + let build_opts = { + let mut build_opts = + cargo_near::commands::build::actions::non_reproducible_wasm::BuildOpts::from( + cli_build_otps, + ); + build_opts.manifest_path = Some(cargo_path.into()); + build_opts + }; + tracing::debug!("non_reproducible_wasm::BuildOpts: {:#?}", build_opts); - }}; + let artifact = + cargo_near::commands::build::actions::non_reproducible_wasm::run(build_opts)?; + artifact.path + } + Some(_) => todo!(), + None => unreachable!(), + }; + Ok(path) } #[macro_export] macro_rules! generate_abi_with { ($(Cargo: $cargo_path:expr;)? $(Vars: $cargo_vars:expr;)? $(Opts: $cli_opts:expr;)? Code: $($code:tt)*) => {{ - let opts = "cargo near abi --no-locked"; - $(let opts = format!("cargo near abi --no-locked {}", $cli_opts);)?; - let result_file = $crate::invoke_cargo_near! { - $(Cargo: $cargo_path;)? $(Vars: $cargo_vars;)? - Opts: &opts; - Code: - $($code)* - }; + let opts: String = "cargo near abi".into(); + $(let opts = format!("cargo near abi {}", $cli_opts);)?; + + let cargo_vars: std::collections::HashMap<&str, String> = std::collections::HashMap::new(); + $(let cargo_vars = $cargo_vars)?; + + let cargo_path: Option<&str> = None; + $(let cargo_path = Some($cargo_path))?; + + let lib_rs_file = syn::parse_file("e::quote! { $($code)* }.to_string()).unwrap(); + + let result_file = $crate::invoke_cargo_near( + function_name!(), + cargo_path, + cargo_vars, + lib_rs_file, + opts, + )?; let result_dir = result_file.as_std_path().parent().expect("has parent"); let abi_root: cargo_near_build::near_abi::AbiRoot = @@ -178,14 +225,24 @@ 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 --no-docker --no-locked"; - $(let opts = format!("cargo near build --no-docker --no-locked {}", $cli_opts);)?; - let result_file = $crate::invoke_cargo_near! { - $(Cargo: $cargo_path;)? $(Vars: $cargo_vars;)? - Opts: &opts; - Code: - $($code)* - }; + let opts: String = "cargo near build non-reproducible-wasm".into(); + $(let opts = format!("cargo near build non-reproducible-wasm {}", $cli_opts);)?; + + let cargo_vars: std::collections::HashMap<&str, String> = std::collections::HashMap::new(); + $(let cargo_vars = $cargo_vars)?; + + let cargo_path: Option<&str> = None; + $(let cargo_path = Some($cargo_path))?; + + let lib_rs_file = syn::parse_file("e::quote! { $($code)* }.to_string()).unwrap(); + + let result_file = $crate::invoke_cargo_near( + function_name!(), + cargo_path, + cargo_vars, + lib_rs_file, + opts, + )?; let result_dir = result_file.as_std_path().parent().expect("has parent"); let wasm_path = result_dir. diff --git a/integration-tests/tests/cargo/mod.rs b/integration-tests/tests/cargo/mod.rs index ce2e16bf..24358745 100644 --- a/integration-tests/tests/cargo/mod.rs +++ b/integration-tests/tests/cargo/mod.rs @@ -26,7 +26,7 @@ fn test_dependency_local_path() -> cargo_near::CliResult { // near-sdk = { path = "::path::", features = ["abi"] } let abi_root = generate_abi_fn_with! { Cargo: "/templates/sdk-dependency/_Cargo_local_path.toml"; - Vars: HashMap::from([("path", near_sdk_dep_path.to_str().unwrap())]); + Vars: HashMap::from([("path", near_sdk_dep_path.to_str().unwrap().to_owned())]); Code: pub fn foo(&self, a: bool, b: u32) {} }; @@ -47,7 +47,7 @@ fn test_dependency_local_path_with_version() -> cargo_near::CliResult { let abi_root = generate_abi_fn_with! { Cargo: "/templates/sdk-dependency/_Cargo_local_path_with_version.toml"; - Vars: HashMap::from([("path", near_sdk_dep_path.to_str().unwrap())]); + Vars: HashMap::from([("path", near_sdk_dep_path.to_str().unwrap().to_owned())]); Code: pub fn foo(&self, a: bool, b: u32) {} };