Przeglądaj źródła

cli: Do verifiable builds from any image (#1054)

Jon Cinque 3 lat temu
rodzic
commit
384f143f77
2 zmienionych plików z 230 dodań i 146 usunięć
  1. 15 1
      cli/src/config.rs
  2. 215 145
      cli/src/lib.rs

+ 15 - 1
cli/src/config.rs

@@ -1,7 +1,7 @@
 use anchor_client::Cluster;
 use anchor_syn::idl::Idl;
 use anyhow::{anyhow, Error, Result};
-use clap::Clap;
+use clap::{ArgEnum, Clap};
 use serde::{Deserialize, Serialize};
 use solana_sdk::pubkey::Pubkey;
 use solana_sdk::signature::{Keypair, Signer};
@@ -268,6 +268,20 @@ pub struct WorkspaceConfig {
     pub types: String,
 }
 
+#[derive(ArgEnum, Clap, Clone, PartialEq, Debug)]
+pub enum BootstrapMode {
+    None,
+    Debian,
+}
+
+#[derive(Debug, Clone)]
+pub struct BuildConfig {
+    pub verifiable: bool,
+    pub solana_version: Option<String>,
+    pub docker_image: String,
+    pub bootstrap: BootstrapMode,
+}
+
 impl Config {
     pub fn docker(&self) -> String {
         let ver = self

+ 215 - 145
cli/src/lib.rs

@@ -1,6 +1,6 @@
 use crate::config::{
-    AnchorPackage, Config, ConfigOverride, Manifest, ProgramDeployment, ProgramWorkspace, Test,
-    WithPath,
+    AnchorPackage, BootstrapMode, BuildConfig, Config, ConfigOverride, Manifest, ProgramDeployment,
+    ProgramWorkspace, Test, WithPath,
 };
 use anchor_client::Cluster;
 use anchor_lang::idl::{IdlAccount, IdlInstruction};
@@ -79,6 +79,13 @@ pub enum Command {
         /// only.
         #[clap(short, long)]
         solana_version: Option<String>,
+        /// Docker image to use. For --verifiable builds only.
+        #[clap(short, long)]
+        docker_image: Option<String>,
+        /// Bootstrap docker image from scratch, installing all requirements for
+        /// verifiable builds. Only works for debian-based images.
+        #[clap(arg_enum, short, long, default_value = "none")]
+        bootstrap: BootstrapMode,
         /// Arguments to pass to the underlying `cargo build-bpf` command
         #[clap(
             required = false,
@@ -100,6 +107,13 @@ pub enum Command {
         /// only.
         #[clap(short, long)]
         solana_version: Option<String>,
+        /// Docker image to use. For --verifiable builds only.
+        #[clap(short, long)]
+        docker_image: Option<String>,
+        /// Bootstrap docker image from scratch, installing all requirements for
+        /// verifiable builds. Only works for debian-based images.
+        #[clap(arg_enum, short, long, default_value = "none")]
+        bootstrap: BootstrapMode,
         /// Arguments to pass to the underlying `cargo build-bpf` command.
         #[clap(
             required = false,
@@ -321,6 +335,8 @@ pub fn entry(opts: Opts) -> Result<()> {
             verifiable,
             program_name,
             solana_version,
+            docker_image,
+            bootstrap,
             cargo_args,
         } => build(
             &opts.cfg_override,
@@ -329,6 +345,8 @@ pub fn entry(opts: Opts) -> Result<()> {
             verifiable,
             program_name,
             solana_version,
+            docker_image,
+            bootstrap,
             None,
             None,
             cargo_args,
@@ -337,12 +355,16 @@ pub fn entry(opts: Opts) -> Result<()> {
             program_id,
             program_name,
             solana_version,
+            docker_image,
+            bootstrap,
             cargo_args,
         } => verify(
             &opts.cfg_override,
             program_id,
             program_name,
             solana_version,
+            docker_image,
+            bootstrap,
             cargo_args,
         ),
         Command::Deploy { program_name } => deploy(&opts.cfg_override, program_name),
@@ -543,6 +565,8 @@ pub fn build(
     verifiable: bool,
     program_name: Option<String>,
     solana_version: Option<String>,
+    docker_image: Option<String>,
+    bootstrap: BootstrapMode,
     stdout: Option<File>, // Used for the package registry server.
     stderr: Option<File>, // Used for the package registry server.
     cargo_args: Vec<String>,
@@ -553,6 +577,12 @@ pub fn build(
     }
 
     let cfg = Config::discover(cfg_override)?.expect("Not in workspace.");
+    let build_config = BuildConfig {
+        verifiable,
+        solana_version: solana_version.or_else(|| cfg.solana_version.clone()),
+        docker_image: docker_image.unwrap_or_else(|| cfg.docker()),
+        bootstrap,
+    };
     let cfg_parent = cfg.path().parent().expect("Invalid Anchor.toml");
 
     let cargo = Manifest::discover()?;
@@ -573,11 +603,6 @@ pub fn build(
         fs::create_dir_all(cfg_parent.join(&cfg.workspace.types))?;
     };
 
-    let solana_version = match solana_version.is_some() {
-        true => solana_version,
-        false => cfg.solana_version.clone(),
-    };
-
     match cargo {
         // No Cargo.toml so build the entire workspace.
         None => build_all(
@@ -585,8 +610,7 @@ pub fn build(
             cfg.path(),
             idl_out,
             idl_ts_out,
-            verifiable,
-            solana_version,
+            &build_config,
             stdout,
             stderr,
             cargo_args,
@@ -597,8 +621,7 @@ pub fn build(
             cfg.path(),
             idl_out,
             idl_ts_out,
-            verifiable,
-            solana_version,
+            &build_config,
             stdout,
             stderr,
             cargo_args,
@@ -609,8 +632,7 @@ pub fn build(
             cargo.path().to_path_buf(),
             idl_out,
             idl_ts_out,
-            verifiable,
-            solana_version,
+            &build_config,
             stdout,
             stderr,
             cargo_args,
@@ -628,8 +650,7 @@ fn build_all(
     cfg_path: &Path,
     idl_out: Option<PathBuf>,
     idl_ts_out: Option<PathBuf>,
-    verifiable: bool,
-    solana_version: Option<String>,
+    build_config: &BuildConfig,
     stdout: Option<File>, // Used for the package registry server.
     stderr: Option<File>, // Used for the package registry server.
     cargo_args: Vec<String>,
@@ -644,8 +665,7 @@ fn build_all(
                     p.join("Cargo.toml"),
                     idl_out.clone(),
                     idl_ts_out.clone(),
-                    verifiable,
-                    solana_version.clone(),
+                    build_config,
                     stdout.as_ref().map(|f| f.try_clone()).transpose()?,
                     stderr.as_ref().map(|f| f.try_clone()).transpose()?,
                     cargo_args.clone(),
@@ -665,8 +685,7 @@ fn build_cwd(
     cargo_toml: PathBuf,
     idl_out: Option<PathBuf>,
     idl_ts_out: Option<PathBuf>,
-    verifiable: bool,
-    solana_version: Option<String>,
+    build_config: &BuildConfig,
     stdout: Option<File>,
     stderr: Option<File>,
     cargo_args: Vec<String>,
@@ -675,9 +694,9 @@ fn build_cwd(
         None => return Err(anyhow!("Unable to find parent")),
         Some(p) => std::env::set_current_dir(&p)?,
     };
-    match verifiable {
+    match build_config.verifiable {
         false => _build_cwd(cfg, idl_out, idl_ts_out, cargo_args),
-        true => build_cwd_verifiable(cfg, cargo_toml, solana_version, stdout, stderr, cargo_args),
+        true => build_cwd_verifiable(cfg, cargo_toml, build_config, stdout, stderr, cargo_args),
     }
 }
 
@@ -686,7 +705,7 @@ fn build_cwd(
 fn build_cwd_verifiable(
     cfg: &WithPath<Config>,
     cargo_toml: PathBuf,
-    solana_version: Option<String>,
+    build_config: &BuildConfig,
     stdout: Option<File>,
     stderr: Option<File>,
     cargo_args: Vec<String>,
@@ -707,68 +726,44 @@ fn build_cwd_verifiable(
         cfg,
         container_name,
         cargo_toml,
-        solana_version,
+        build_config,
         stdout,
         stderr,
         cargo_args,
     );
 
-    // Wipe the generated docker-target dir.
-    println!("Cleaning up the docker target directory");
-    let exit = std::process::Command::new("docker")
-        .args(&[
-            "exec",
-            container_name,
-            "rm",
-            "-rf",
-            "/workdir/docker-target",
-        ])
-        .stdout(Stdio::inherit())
-        .stderr(Stdio::inherit())
-        .output()
-        .map_err(|e| anyhow::format_err!("Docker rm docker-target failed: {}", e.to_string()))?;
-    if !exit.status.success() {
-        return Err(anyhow!("Failed to build program"));
-    }
-
-    // Remove the docker image.
-    println!("Removing the docker image");
-    let exit = std::process::Command::new("docker")
-        .args(&["rm", "-f", container_name])
-        .stdout(Stdio::inherit())
-        .stderr(Stdio::inherit())
-        .output()
-        .map_err(|e| anyhow::format_err!("{}", e.to_string()))?;
-    if !exit.status.success() {
-        println!("Unable to remove docker container");
-        std::process::exit(exit.status.code().unwrap_or(1));
-    }
-
-    // Build the idl.
-    println!("Extracting the IDL");
-    if let Ok(Some(idl)) = extract_idl("src/lib.rs") {
-        // Write out the JSON file.
-        println!("Writing the IDL file");
-        let out_file = workspace_dir.join(format!("target/idl/{}.json", idl.name));
-        write_idl(&idl, OutFile::File(out_file))?;
-
-        // Write out the TypeScript type.
-        println!("Writing the .ts file");
-        let ts_file = workspace_dir.join(format!("target/types/{}.ts", idl.name));
-        fs::write(&ts_file, template::idl_ts(&idl)?)?;
-
-        // Copy out the TypeScript type.
-        if !&cfg.workspace.types.is_empty() {
-            fs::copy(
-                ts_file,
-                workspace_dir
-                    .join(&cfg.workspace.types)
-                    .join(idl.name)
-                    .with_extension("ts"),
-            )?;
+    match &result {
+        Err(e) => {
+            eprintln!("Error during Docker build: {:?}", e);
+        }
+        Ok(_) => {
+            // Build the idl.
+            println!("Extracting the IDL");
+            if let Ok(Some(idl)) = extract_idl("src/lib.rs") {
+                // Write out the JSON file.
+                println!("Writing the IDL file");
+                let out_file = workspace_dir.join(format!("target/idl/{}.json", idl.name));
+                write_idl(&idl, OutFile::File(out_file))?;
+
+                // Write out the TypeScript type.
+                println!("Writing the .ts file");
+                let ts_file = workspace_dir.join(format!("target/types/{}.ts", idl.name));
+                fs::write(&ts_file, template::idl_ts(&idl)?)?;
+
+                // Copy out the TypeScript type.
+                if !&cfg.workspace.types.is_empty() {
+                    fs::copy(
+                        ts_file,
+                        workspace_dir
+                            .join(&cfg.workspace.types)
+                            .join(idl.name)
+                            .with_extension("ts"),
+                    )?;
+                }
+            }
+            println!("Build success");
         }
     }
-    println!("Build success");
 
     result
 }
@@ -777,7 +772,7 @@ fn docker_build(
     cfg: &WithPath<Config>,
     container_name: &str,
     cargo_toml: PathBuf,
-    solana_version: Option<String>,
+    build_config: &BuildConfig,
     stdout: Option<File>,
     stderr: Option<File>,
     cargo_args: Vec<String>,
@@ -785,14 +780,16 @@ fn docker_build(
     let binary_name = Manifest::from_path(&cargo_toml)?.lib_name()?;
 
     // Docker vars.
-    let image_name = cfg.docker();
+    let workdir = Path::new("/workdir");
     let volume_mount = format!(
-        "{}:/workdir",
-        cfg.path().parent().unwrap().canonicalize()?.display()
+        "{}:{}",
+        cfg.path().parent().unwrap().canonicalize()?.display(),
+        workdir.to_str().unwrap(),
     );
-    println!("Using image {:?}", image_name);
+    println!("Using image {:?}", build_config.docker_image);
 
     // Start the docker image running detached in the background.
+    let target_dir = workdir.join("docker-target");
     println!("Run docker image");
     let exit = std::process::Command::new("docker")
         .args(&[
@@ -802,10 +799,15 @@ fn docker_build(
             "--name",
             container_name,
             "--env",
-            "CARGO_TARGET_DIR=/workdir/docker-target",
+            &format!(
+                "CARGO_TARGET_DIR={}",
+                target_dir.as_path().to_str().unwrap()
+            ),
             "-v",
             &volume_mount,
-            &image_name,
+            "-w",
+            workdir.to_str().unwrap(),
+            &build_config.docker_image,
             "bash",
         ])
         .stdout(Stdio::inherit())
@@ -816,58 +818,84 @@ fn docker_build(
         return Err(anyhow!("Failed to build program"));
     }
 
+    let result = docker_prep(container_name, build_config).and_then(|_| {
+        let cfg_parent = cfg.path().parent().unwrap();
+        docker_build_bpf(
+            container_name,
+            cargo_toml.as_path(),
+            cfg_parent,
+            target_dir.as_path(),
+            binary_name,
+            stdout,
+            stderr,
+            cargo_args,
+        )
+    });
+
+    // Cleanup regardless of errors
+    docker_cleanup(container_name, target_dir.as_path())?;
+
+    // Done.
+    result
+}
+
+fn docker_prep(container_name: &str, build_config: &BuildConfig) -> Result<()> {
     // Set the solana version in the container, if given. Otherwise use the
     // default.
-    if let Some(solana_version) = solana_version {
-        println!("Using solana version: {}", solana_version);
+    match build_config.bootstrap {
+        BootstrapMode::Debian => {
+            // Install build requirements
+            docker_exec(container_name, &["apt", "update"])?;
+            docker_exec(
+                container_name,
+                &["apt", "install", "-y", "curl", "build-essential"],
+            )?;
 
-        // Fetch the installer.
-        let exit = std::process::Command::new("docker")
-            .args(&[
-                "exec",
+            // Install Rust
+            docker_exec(
                 container_name,
+                &["curl", "https://sh.rustup.rs", "-sfo", "rustup.sh"],
+            )?;
+            docker_exec(container_name, &["sh", "rustup.sh", "-y"])?;
+            docker_exec(container_name, &["rm", "-f", "rustup.sh"])?;
+        }
+        BootstrapMode::None => {}
+    }
+
+    if let Some(solana_version) = &build_config.solana_version {
+        println!("Using solana version: {}", solana_version);
+
+        // Install Solana CLI
+        docker_exec(
+            container_name,
+            &[
                 "curl",
                 "-sSfL",
                 &format!("https://release.solana.com/v{0}/install", solana_version,),
                 "-o",
                 "solana_installer.sh",
-            ])
-            .stdout(Stdio::inherit())
-            .stderr(Stdio::inherit())
-            .output()
-            .map_err(|e| anyhow!("Failed to set solana version: {:?}", e))?;
-        if !exit.status.success() {
-            return Err(anyhow!("Failed to set solana version"));
-        }
-
-        // Run the installer.
-        let exit = std::process::Command::new("docker")
-            .args(&["exec", container_name, "sh", "solana_installer.sh"])
-            .stdout(Stdio::inherit())
-            .stderr(Stdio::inherit())
-            .output()
-            .map_err(|e| anyhow!("Failed to set solana version: {:?}", e))?;
-        if !exit.status.success() {
-            return Err(anyhow!("Failed to set solana version"));
-        }
-
-        // Remove the installer.
-        let exit = std::process::Command::new("docker")
-            .args(&["exec", container_name, "rm", "-f", "solana_installer.sh"])
-            .stdout(Stdio::inherit())
-            .stderr(Stdio::inherit())
-            .output()
-            .map_err(|e| anyhow!("Failed to remove installer: {:?}", e))?;
-        if !exit.status.success() {
-            return Err(anyhow!("Failed to remove installer"));
-        }
+            ],
+        )?;
+        docker_exec(container_name, &["sh", "solana_installer.sh"])?;
+        docker_exec(container_name, &["rm", "-f", "solana_installer.sh"])?;
     }
+    Ok(())
+}
 
-    let manifest_path = pathdiff::diff_paths(
-        cargo_toml.canonicalize()?,
-        cfg.path().parent().unwrap().canonicalize()?,
-    )
-    .ok_or_else(|| anyhow!("Unable to diff paths"))?;
+#[allow(clippy::too_many_arguments)]
+fn docker_build_bpf(
+    container_name: &str,
+    cargo_toml: &Path,
+    cfg_parent: &Path,
+    target_dir: &Path,
+    binary_name: String,
+    stdout: Option<File>,
+    stderr: Option<File>,
+    cargo_args: Vec<String>,
+) -> Result<()> {
+    let manifest_path =
+        pathdiff::diff_paths(cargo_toml.canonicalize()?, cfg_parent.canonicalize()?)
+            .ok_or_else(|| anyhow!("Unable to diff paths"))?;
     println!(
         "Building {} manifest: {:?}",
         binary_name,
@@ -878,6 +906,8 @@ fn docker_build(
     let exit = std::process::Command::new("docker")
         .args(&[
             "exec",
+            "--env",
+            "PATH=/root/.local/share/solana/install/active_release/bin:/root/.cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
             container_name,
             "cargo",
             "build-bpf",
@@ -901,10 +931,7 @@ fn docker_build(
 
     // Copy the binary out of the docker image.
     println!("Copying out the build artifacts");
-    let out_file = cfg
-        .path()
-        .parent()
-        .unwrap()
+    let out_file = cfg_parent
         .canonicalize()?
         .join(format!("target/verifiable/{}.so", binary_name))
         .display()
@@ -912,9 +939,12 @@ fn docker_build(
 
     // This requires the target directory of any built program to be located at
     // the root of the workspace.
+    let mut bin_path = target_dir.join("deploy");
+    bin_path.push(format!("{}.so", binary_name));
     let bin_artifact = format!(
-        "{}:/workdir/docker-target/deploy/{}.so",
-        container_name, binary_name
+        "{}:{}",
+        container_name,
+        bin_path.as_path().to_str().unwrap()
     );
     let exit = std::process::Command::new("docker")
         .args(&["cp", &bin_artifact, &out_file])
@@ -923,15 +953,48 @@ fn docker_build(
         .output()
         .map_err(|e| anyhow::format_err!("{}", e.to_string()))?;
     if !exit.status.success() {
-        return Err(anyhow!(
+        Err(anyhow!(
             "Failed to copy binary out of docker. Is the target directory set correctly?"
-        ));
+        ))
+    } else {
+        Ok(())
     }
+}
 
-    // Done.
+fn docker_cleanup(container_name: &str, target_dir: &Path) -> Result<()> {
+    // Wipe the generated docker-target dir.
+    println!("Cleaning up the docker target directory");
+    docker_exec(container_name, &["rm", "-rf", target_dir.to_str().unwrap()])?;
+
+    // Remove the docker image.
+    println!("Removing the docker image");
+    let exit = std::process::Command::new("docker")
+        .args(&["rm", "-f", container_name])
+        .stdout(Stdio::inherit())
+        .stderr(Stdio::inherit())
+        .output()
+        .map_err(|e| anyhow::format_err!("{}", e.to_string()))?;
+    if !exit.status.success() {
+        println!("Unable to remove docker container");
+        std::process::exit(exit.status.code().unwrap_or(1));
+    }
     Ok(())
 }
 
+fn docker_exec(container_name: &str, args: &[&str]) -> Result<()> {
+    let exit = std::process::Command::new("docker")
+        .args([&["exec", container_name], args].concat())
+        .stdout(Stdio::inherit())
+        .stderr(Stdio::inherit())
+        .output()
+        .map_err(|e| anyhow!("Failed to run command \"{:?}\": {:?}", args, e))?;
+    if !exit.status.success() {
+        Err(anyhow!("Failed to run command: {:?}", args))
+    } else {
+        Ok(())
+    }
+}
+
 fn _build_cwd(
     cfg: &WithPath<Config>,
     idl_out: Option<PathBuf>,
@@ -987,6 +1050,8 @@ fn verify(
     program_id: Pubkey,
     program_name: Option<String>,
     solana_version: Option<String>,
+    docker_image: Option<String>,
+    bootstrap: BootstrapMode,
     cargo_args: Vec<String>,
 ) -> Result<()> {
     // Change to the workspace member directory, if needed.
@@ -1002,16 +1067,15 @@ fn verify(
     let cur_dir = std::env::current_dir()?;
     build(
         cfg_override,
-        None,
-        None,
-        true,
-        None,
-        match solana_version.is_some() {
-            true => solana_version,
-            false => cfg.solana_version.clone(),
-        },
-        None,
-        None,
+        None,                                                  // idl
+        None,                                                  // idl ts
+        true,                                                  // verifiable
+        None,                                                  // program name
+        solana_version.or_else(|| cfg.solana_version.clone()), // solana version
+        docker_image,                                          // docker image
+        bootstrap,                                             // bootstrap docker image
+        None,                                                  // stdout
+        None,                                                  // stderr
         cargo_args,
     )?;
     std::env::set_current_dir(&cur_dir)?;
@@ -1526,6 +1590,8 @@ fn test(
                 None,
                 None,
                 None,
+                BootstrapMode::None,
+                None,
                 None,
                 cargo_args,
             )?;
@@ -2432,7 +2498,9 @@ fn publish(
         None,
         true,
         Some(program_name),
-        cfg.solana_version.clone(),
+        None,
+        None,
+        BootstrapMode::None,
         None,
         None,
         cargo_args,
@@ -2529,6 +2597,8 @@ fn localnet(
                 None,
                 None,
                 None,
+                BootstrapMode::None,
+                None,
                 None,
                 cargo_args,
             )?;