Repository: OffchainLabs/cargo-stylus (language: rust)

Issue:
[feat] `cargo stylus init` request
Hi all,

Thanks for all the great work! 
I thought to make a quick feature request for a `cargo stylus init` command, which would ideally follow the pattern of `cargo new` and `cargo init`, allowing `cargo stylus` to initialize a project in an existing folder (with an existing repo). I'd also be happy to help with this if needed. :)

Happy friday ~



Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index bdd0228..fcca0c6 100644
--- a/README.md
+++ b/README.md
@@ -156,24 +156,7 @@ See `--help` for all available flags and default values.
 
 ## Verifying Stylus Contracts
 
-**cargo stylus verify**
-
-Verifies that a deployed smart contract is identical to that produced by the
-current project. Since Stylus smart contracts include a hash of all project
-files, this additionally verifies that code comments and other files are
-identical. To ensure build reproducibility, if a contract is to be verified,
-it should be both deployed and verified using `cargo stylus reproducible`.
-
-See `--help` for all available flags and default values.
-
-## Reproducibly Deploying and Verifying
-
-**cargo stylus reproducible**
-
-Runs a `cargo stylus` command in a Docker container to ensure build
-reproducibility.
-
-See `--help` for all available flags and default values.
+See [here](https://hackmd.io/bpeMnrzbSvO4mohhvkrKqw)
 
 ## Deploying Non-Rust WASM Projects
 
diff --git a/check/Dockerfile b/check/Dockerfile
deleted file mode 100644
index cea0606..0000000
--- a/check/Dockerfile
+++ /dev/null
@@ -1,7 +0,0 @@
-FROM rust:1.71 as builder
-COPY . .
-RUN cargo build --release
-
-FROM debian:buster-slim
-COPY --from=builder ./target/release/cargo-stylus ./target/release/cargo-stylus
-CMD ["/target/release/cargo-stylus"]
\ No newline at end of file
diff --git a/check/src/cache.rs b/check/src/cache.rs
index 7d907e1..f850e50 100644
--- a/check/src/cache.rs
+++ b/check/src/cache.rs
@@ -30,6 +30,7 @@ sol! {
         error AlreadyCached(bytes32 codehash);
         error BidTooSmall(uint192 bid, uint192 min);
         error BidsArePaused();
+        error ProgramNotActivated();
     }
 }
 
@@ -88,6 +89,9 @@ pub async fn cache_contract(cfg: &CacheConfig) -> Result<()> {
             C::BidTooSmall(_) => {
                 bail!("Bid amount {} (wei) too small", cfg.bid.unwrap_or_default())
             }
+            C::ProgramNotActivated(_) => {
+                bail!("Your Stylus contract {} is not yet activated. To activate it, use the `cargo stylus activate` subcommand", hex::encode(contract))
+            }
         }
     }
     let verbose = cfg.common_cfg.verbose;
diff --git a/check/src/check.rs b/check/src/check.rs
index 0716e62..e315d03 100644
--- a/check/src/check.rs
+++ b/check/src/check.rs
@@ -210,6 +210,12 @@ async fn contract_exists(codehash: B256, provider: &Provider<Http>) -> Result<bo
 
     let program_version = match outs {
         Ok(outs) => {
+            if outs.is_empty() {
+                bail!(
+                    r#"No data returned from the ArbWasm precompile when checking if your Stylus contract exists.
+Perhaps the Arbitrum node for the endpoint you are connecting to has not yet upgraded to Stylus"#
+                );
+            }
             let ArbWasm::codehashVersionReturn { version } =
                 ArbWasm::codehashVersionCall::abi_decode_returns(&outs, true)?;
             version
diff --git a/check/src/constants.rs b/check/src/constants.rs
index b923821..935d9b9 100644
--- a/check/src/constants.rs
+++ b/check/src/constants.rs
@@ -43,8 +43,3 @@ pub const PROJECT_HASH_SECTION_NAME: &str = "project_hash";
 
 /// Name of the toolchain file used to specify the Rust toolchain version for a project.
 pub const TOOLCHAIN_FILE_NAME: &str = "rust-toolchain.toml";
-
-/// Base Rust image version to be used for reproducible builds. This simply installs cargo and the Rust
-/// compiler, but the user will specify the exact version of the Rust toolchain to use for building within
-/// the docker container.
-pub const RUST_BASE_IMAGE_VERSION: &str = "1.79.0";
diff --git a/check/src/deploy.rs b/check/src/deploy.rs
index f146e0b..9c5d79e 100644
--- a/check/src/deploy.rs
+++ b/check/src/deploy.rs
@@ -146,7 +146,7 @@ impl DeployConfig {
         let tx_hash = receipt.transaction_hash.debug_lavender();
         greyln!("deployment tx hash: {tx_hash}");
         println!(
-            r#"we recommend running cargo stylus cache --address={} to cache your activated contract in ArbOS.
+            r#"INFO: Your program is not yet part of the Stylus contract cache. We recommend running `cargo stylus cache --address={}` to cache your activated contract in ArbOS.
 Cached contracts benefit from cheaper calls. To read more about the Stylus contract cache, see
 https://docs.arbitrum.io/stylus/concepts/stylus-cache-manager"#,
             hex::encode(contract)
diff --git a/check/src/docker.rs b/check/src/docker.rs
index 89a433d..fa96700 100644
--- a/check/src/docker.rs
+++ b/check/src/docker.rs
@@ -8,7 +8,7 @@ use std::process::{Command, Stdio};
 use cargo_stylus_util::color::Color;
 use eyre::{bail, eyre, Result};
 
-use crate::constants::{RUST_BASE_IMAGE_VERSION, TOOLCHAIN_FILE_NAME};
+use crate::constants::TOOLCHAIN_FILE_NAME;
 use crate::macros::greyln;
 use crate::project::extract_toolchain_channel;
 
@@ -45,15 +45,7 @@ fn create_image(version: &str) -> Result<()> {
     if image_exists(&name)? {
         return Ok(());
     }
-    let cargo_stylus_version = env!("CARGO_PKG_VERSION");
-    let cargo_stylus_version: String = cargo_stylus_version
-        .chars()
-        .filter(|c| c.is_alphanumeric() || *c == '-' || *c == '.')
-        .collect();
-    println!(
-        "Building Docker image for cargo-stylus version {} and Rust toolchain {}",
-        cargo_stylus_version, version,
-    );
+    println!("Building Docker image for Rust toolchain {}", version,);
     let mut child = Command::new("docker")
         .arg("build")
         .arg("-t")
@@ -62,24 +54,19 @@ fn create_image(version: &str) -> Result<()> {
         .arg("-f-")
         .stdin(Stdio::piped())
         .spawn()
-        .map_err(|e| eyre!("failed to execure Docker command: {e}"))?;
+        .map_err(|e| eyre!("failed to execute Docker command: {e}"))?;
     write!(
         child.stdin.as_mut().unwrap(),
         "\
-            FROM --platform=linux/amd64 rust:{} as builder\n\
+            FROM --platform=linux/amd64 offchainlabs/cargo-stylus-base as base
             RUN rustup toolchain install {}-x86_64-unknown-linux-gnu 
             RUN rustup default {}-x86_64-unknown-linux-gnu
             RUN rustup target add wasm32-unknown-unknown
-            RUN rustup target add wasm32-wasi
-            RUN rustup target add x86_64-unknown-linux-gnu
-            RUN cargo install cargo-stylus-check --version {} --force
-            RUN cargo install cargo-stylus --version {} --force
+            RUN rustup component add rust-src --toolchain {}-x86_64-unknown-linux-gnu
         ",
-        RUST_BASE_IMAGE_VERSION,
         version,
         version,
-        cargo_stylus_version,
-        cargo_stylus_version,
+        version,
     )?;
     child.wait().map_err(|e| eyre!("wait failed: {e}"))?;
     Ok(())
diff --git a/check/src/main.rs b/check/src/main.rs
index 9f8912b..848bf93 100644
--- a/check/src/main.rs
+++ b/check/src/main.rs
@@ -34,7 +34,7 @@ struct Opts {
 
 #[derive(Parser, Debug, Clone)]
 enum Apis {
-    /// Create a new Rust project.
+    /// Create a new Stylus project.
     New {
         /// Project name.
         name: PathBuf,
@@ -42,6 +42,12 @@ enum Apis {
         #[arg(long)]
         minimal: bool,
     },
+    /// Initializes a Stylus project in the current directory.
+    Init {
+        /// Create a minimal contract.
+        #[arg(long)]
+        minimal: bool,
+    },
     /// Export a Solidity ABI.
     ExportAbi {
         /// The output file (defaults to stdout).
@@ -127,10 +133,6 @@ pub struct CheckConfig {
     /// Where to deploy and activate the contract (defaults to a random address).
     #[arg(long)]
     contract_address: Option<H160>,
-    /// If specified, will not run the command in a reproducible docker container. Useful for local
-    /// builds, but at the risk of not having a reproducible contract for verification purposes.
-    #[arg(long)]
-    no_verify: bool,
 }
 
 #[derive(Args, Clone, Debug)]
@@ -143,6 +145,10 @@ struct DeployConfig {
     /// Only perform gas estimation.
     #[arg(long)]
     estimate_gas: bool,
+    /// If specified, will not run the command in a reproducible docker container. Useful for local
+    /// builds, but at the risk of not having a reproducible contract for verification purposes.
+    #[arg(long)]
+    no_verify: bool,
 }
 
 #[derive(Args, Clone, Debug)]
@@ -206,7 +212,7 @@ impl fmt::Display for CheckConfig {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         write!(
             f,
-            "{} {} {} {}",
+            "{} {} {}",
             self.common_cfg,
             match &self.wasm_file {
                 Some(path) => format!("--wasm-file={}", path.display()),
@@ -216,10 +222,6 @@ impl fmt::Display for CheckConfig {
                 Some(addr) => format!("--contract-address={:?}", addr),
                 None => "".to_string(),
             },
-            match self.no_verify {
-                true => "--no-verify".to_string(),
-                false => "".to_string(),
-            },
         )
     }
 }
@@ -228,13 +230,17 @@ impl fmt::Display for DeployConfig {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         write!(
             f,
-            "{} {} {}",
+            "{} {} {} {}",
             self.check_config,
             self.auth,
             match self.estimate_gas {
                 true => "--estimate-gas".to_string(),
                 false => "".to_string(),
             },
+            match self.no_verify {
+                true => "--no-verify".to_string(),
+                false => "".to_string(),
+            },
         )
     }
 }
@@ -296,6 +302,9 @@ async fn main_impl(args: Opts) -> Result<()> {
         Apis::New { name, minimal } => {
             run!(new::new(&name, minimal), "failed to open new project");
         }
+        Apis::Init { minimal } => {
+            run!(new::init(minimal), "failed to initialize project");
+        }
         Apis::ExportAbi { json, output } => {
             run!(export_abi::export_abi(output, json), "failed to export abi");
         }
@@ -309,28 +318,16 @@ async fn main_impl(args: Opts) -> Result<()> {
             run!(cache::cache_contract(&config).await, "stylus cache failed");
         }
         Apis::Check(config) => {
-            if config.no_verify {
-                run!(check::check(&config).await, "stylus checks failed");
-            } else {
-                let mut commands: Vec<String> =
-                    vec![String::from("check"), String::from("--no-verify")];
-                let config_args = config
-                    .to_string()
-                    .split(' ')
-                    .map(|s| s.to_string())
-                    .filter(|s| !s.is_empty())
-                    .collect::<Vec<String>>();
-                commands.extend(config_args);
-                run!(
-                    docker::run_reproducible(&commands),
-                    "failed reproducible run"
-                );
-            }
+            run!(check::check(&config).await, "stylus checks failed");
         }
         Apis::Deploy(config) => {
-            if config.check_config.no_verify {
+            if config.no_verify {
                 run!(deploy::deploy(config).await, "stylus deploy failed");
             } else {
+                println!(
+                    "Running in a Docker container for reproducibility, this may take a while",
+                );
+                println!("NOTE: You can opt out by doing --no-verify");
                 let mut commands: Vec<String> =
                     vec![String::from("deploy"), String::from("--no-verify")];
                 let config_args = config
@@ -350,6 +347,9 @@ async fn main_impl(args: Opts) -> Result<()> {
             if config.no_verify {
                 run!(verify::verify(config).await, "failed to verify");
             } else {
+                println!(
+                    "Running in a Docker container for reproducibility, this may take a while",
+                );
                 let mut commands: Vec<String> =
                     vec![String::from("verify"), String::from("--no-verify")];
                 let config_args = config
diff --git a/check/src/new.rs b/check/src/new.rs
index 668df01..7fe0e1c 100644
--- a/check/src/new.rs
+++ b/check/src/new.rs
@@ -29,3 +29,31 @@ pub fn new(name: &Path, minimal: bool) -> Result<()> {
     println!("{GREY}new project at: {}", path.to_string_lossy().mint());
     Ok(())
 }
+
+pub fn init(minimal: bool) -> Result<()> {
+    let current_dir = current_dir().wrap_err("no current dir")?;
+    let repo = if minimal {
+        GITHUB_TEMPLATE_REPO_MINIMAL
+    } else {
+        GITHUB_TEMPLATE_REPO
+    };
+
+    let output = sys::new_command("git")
+        .arg("clone")
+        .arg("--depth")
+        .arg("1")
+        .arg(repo)
+        .arg(".")
+        .output()
+        .wrap_err("git clone failed")?;
+
+    if !output.status.success() {
+        bail!("git clone command failed");
+    }
+
+    println!(
+        "{GREY}initialized project in: {}",
+        current_dir.to_string_lossy().mint()
+    );
+    Ok(())
+}
diff --git a/check/src/project.rs b/check/src/project.rs
index ee327ec..8ea07ab 100644
--- a/check/src/project.rs
+++ b/check/src/project.rs
@@ -57,10 +57,6 @@ pub fn build_dylib(cfg: BuildConfig) -> Result<PathBuf> {
 
     let mut cmd = sys::new_command("cargo");
 
-    if !cfg.stable {
-        cmd.arg("+nightly");
-    }
-
     cmd.arg("build");
     cmd.arg("--lib");
 
@@ -208,9 +204,6 @@ pub fn extract_toolchain_channel(toolchain_file_path: &PathBuf) -> Result<String
 pub fn hash_files(source_file_patterns: Vec<String>, cfg: BuildConfig) -> Result<[u8; 32]> {
     let mut keccak = Keccak::v256();
     let mut cmd = Command::new("cargo");
-    if !cfg.stable {
-        cmd.arg("+nightly");
-    }
     cmd.arg("--version");
     let output = cmd
         .output()
diff --git a/check/src/verify.rs b/check/src/verify.rs
index 9c55f8b..2cc0084 100644
--- a/check/src/verify.rs
+++ b/check/src/verify.rs
@@ -54,7 +54,6 @@ pub async fn verify(cfg: VerifyConfig) -> eyre::Result<()> {
         common_cfg: cfg.common_cfg.clone(),
         wasm_file: None,
         contract_address: None,
-        no_verify: cfg.no_verify,
     };
     let _ = check::check(&check_cfg)
         .await
diff --git a/main/src/main.rs b/main/src/main.rs
index 5240e1b..ab9a655 100644
--- a/main/src/main.rs
+++ b/main/src/main.rs
@@ -28,8 +28,11 @@ struct Opts {
 #[derive(Parser, Debug, Clone)]
 enum Subcommands {
     #[command(alias = "n")]
-    /// Create a new Rust project.
+    /// Create a new Stylus project.
     New,
+    #[command(alias = "i")]
+    /// Initializes a Stylus project in the current directory.
+    Init,
     #[command(alias = "x")]
     /// Export a Solidity ABI.
     ExportAbi,
@@ -69,12 +72,15 @@ const COMMANDS: &[Binary] = &[
         name: "cargo-stylus-check",
         apis: &[
             "new",
+            "init",
+            "activate",
             "export-abi",
             "cache",
             "check",
             "deploy",
             "verify",
             "a",
+            "i",
             "n",
             "x",
             "c",
diff --git a/replay/src/main.rs b/replay/src/main.rs
index e74b465..79fd1da 100644
--- a/replay/src/main.rs
+++ b/replay/src/main.rs
@@ -147,7 +147,7 @@ async fn replay(args: ReplayArgs) -> Result<()> {
     let provider = sys::new_provider(&args.endpoint)?;
     let trace = Trace::new(provider, args.tx).await?;
 
-    build_so(&args.project, args.stable_rust)?;
+    build_so(&args.project)?;
     let so = find_so(&args.project)?;
 
     // TODO: don't assume the contract is top-level
@@ -169,12 +169,9 @@ async fn replay(args: ReplayArgs) -> Result<()> {
     Ok(())
 }
 
-pub fn build_so(path: &Path, stable: bool) -> Result<()> {
+pub fn build_so(path: &Path) -> Result<()> {
     let mut cargo = sys::new_command("cargo");
 
-    if !stable {
-        cargo.arg("+nightly");
-    }
     cargo
         .current_dir(path)
         .arg("build")


Test patch: