contract_build/
docker.rs

1// Copyright (C) Use Ink (UK) Ltd.
2// This file is part of cargo-contract.
3//
4// cargo-contract is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// cargo-contract is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with cargo-contract.  If not, see <http://www.gnu.org/licenses/>.
16
17//! This module provides a simple interface to execute the verifiable build
18//! inside the docker container.
19//!
20//! For the correct behaviour, the docker engine must be running,
21//! and the socket to be accessible.
22//!
23//! It is also important that the docker registry contains the tag
24//! that matches the current version of this crate.
25//!
26//! The process of the build is following:
27//! 1. Pull the image from the registry or use the local copy if available
28//! 2. Parse other arguments that were passed to the host execution context
29//! 3. Calculate the digest of the command and use it to uniquely identify the container
30//! 4. If the container exists, we just start the build, if not, we create it
31//! 5. After the build, the docker container produces metadata with paths relative to its
32//!    internal storage structure, we parse the file and overwrite those paths relative to
33//!    the host machine.
34
35use std::{
36    cmp::Ordering,
37    collections::{
38        hash_map::DefaultHasher,
39        HashMap,
40    },
41    hash::{
42        Hash,
43        Hasher,
44    },
45    io::{
46        BufReader,
47        Write,
48    },
49    marker::Unpin,
50    path::Path,
51};
52
53use anyhow::{
54    Context,
55    Result,
56};
57use bollard::{
58    container::{
59        AttachContainerOptions,
60        AttachContainerResults,
61        Config,
62        CreateContainerOptions,
63        ListContainersOptions,
64        LogOutput,
65        RemoveContainerOptions,
66    },
67    errors::Error,
68    image::{
69        CreateImageOptions,
70        ListImagesOptions,
71    },
72    models::CreateImageInfo,
73    service::{
74        HostConfig,
75        ImageSummary,
76        Mount,
77        MountTypeEnum,
78    },
79    Docker,
80};
81use contract_metadata::ContractMetadata;
82use tokio_stream::{
83    Stream,
84    StreamExt,
85};
86
87use crate::{
88    verbose_eprintln,
89    BuildResult,
90    CrateMetadata,
91    ExecuteArgs,
92    Verbosity,
93};
94
95use colored::Colorize;
96/// Default image to be used for the build.
97const IMAGE: &str = "useink/contracts-verifiable";
98/// We assume the docker image contains the same tag as the current version of the crate.
99const VERSION: &str = env!("CARGO_PKG_VERSION");
100/// The default directory to be mounted in the container.
101const MOUNT_DIR: &str = "/contract";
102
103/// The image to be used.
104#[derive(Clone, Debug, Default)]
105pub enum ImageVariant {
106    /// The default image is used, specified in the `IMAGE` constant.
107    #[default]
108    Default,
109    /// Custom image is used.
110    Custom(String),
111}
112
113impl From<Option<String>> for ImageVariant {
114    fn from(value: Option<String>) -> Self {
115        if let Some(image) = value {
116            ImageVariant::Custom(image)
117        } else {
118            ImageVariant::Default
119        }
120    }
121}
122
123/// Launches the docker container to execute verifiable build.
124pub fn docker_build(args: ExecuteArgs) -> Result<BuildResult> {
125    let ExecuteArgs {
126        manifest_path,
127        verbosity,
128        output_type,
129        target,
130        image,
131        ..
132    } = args;
133    tokio::runtime::Builder::new_multi_thread()
134        .enable_all()
135        .build()?
136        .block_on(async {
137            let crate_metadata = CrateMetadata::collect(&manifest_path, target)?;
138            let host_folder = std::env::current_dir()?;
139            let args = compose_build_args()?;
140
141            let client = Docker::connect_with_socket_defaults().map_err(|e| {
142                anyhow::anyhow!("{}\nDo you have the docker engine installed in path?", e)
143            })?;
144            let _ = client.ping().await.map_err(|e| {
145                anyhow::anyhow!("{}\nIs your docker engine up and running?", e)
146            })?;
147
148            let image = match image {
149                ImageVariant::Custom(i) => i.clone(),
150                ImageVariant::Default => {
151                    format!("{}:{}", IMAGE, VERSION)
152                }
153            };
154
155            let container = create_container(
156                &client,
157                args.clone(),
158                &image,
159                &crate_metadata.contract_artifact_name,
160                &host_folder,
161                &verbosity,
162            )
163            .await?;
164
165            let build_result = async {
166                let mut build_result = run_build(&client, &container, &verbosity).await?;
167                update_build_result(&host_folder, &mut build_result)?;
168                update_metadata(&build_result, &verbosity, &image, &client).await?;
169                Ok::<BuildResult, anyhow::Error>(build_result)
170            }
171            .await;
172
173            let build_result = match build_result {
174                Ok(build_result) => build_result,
175                Err(e) => {
176                    // Remove container to avoid leaving it in an incorrect state for
177                    // subsequent calls
178                    let options = Some(RemoveContainerOptions {
179                        force: true,
180                        ..Default::default()
181                    });
182                    let _ = client.remove_container(&container, options).await;
183                    return Err(e)
184                }
185            };
186
187            verbose_eprintln!(
188                verbosity,
189                " {} {}",
190                "[==]".bold(),
191                "Displaying results".bright_cyan().bold(),
192            );
193
194            Ok(BuildResult {
195                output_type,
196                verbosity,
197                ..build_result
198            })
199        })
200}
201
202/// Updates `build_result` paths to the artefacts.
203fn update_build_result(host_folder: &Path, build_result: &mut BuildResult) -> Result<()> {
204    let new_path = host_folder.join(
205        build_result
206            .target_directory
207            .as_path()
208            .strip_prefix(MOUNT_DIR)?,
209    );
210    build_result.target_directory = new_path;
211
212    let new_path = build_result.dest_wasm.as_ref().map(|p| {
213        host_folder.join(
214            p.as_path()
215                .strip_prefix(MOUNT_DIR)
216                .expect("cannot strip prefix"),
217        )
218    });
219    build_result.dest_wasm = new_path;
220
221    // TODO: Clippy currently throws a false-positive here. The manual allow can be
222    // removed after https://github.com/rust-lang/rust-clippy/pull/13609 has been released.
223    #[allow(clippy::manual_inspect)]
224    build_result.metadata_result.as_mut().map(|m| {
225        m.dest_bundle = host_folder.join(
226            m.dest_bundle
227                .as_path()
228                .strip_prefix(MOUNT_DIR)
229                .expect("cannot strip prefix"),
230        );
231        m.dest_metadata = host_folder.join(
232            m.dest_metadata
233                .as_path()
234                .strip_prefix(MOUNT_DIR)
235                .expect("cannot strip prefix"),
236        );
237        m
238    });
239    Ok(())
240}
241
242/// Overwrites `build_result` and `image` fields in the metadata.
243async fn update_metadata(
244    build_result: &BuildResult,
245    verbosity: &Verbosity,
246    build_image: &str,
247    client: &Docker,
248) -> Result<()> {
249    if let Some(metadata_artifacts) = &build_result.metadata_result {
250        let mut metadata = ContractMetadata::load(&metadata_artifacts.dest_bundle)?;
251
252        let build_image = find_local_image(client, build_image.to_string())
253            .await?
254            .context("Image summary does not exist")?;
255        // find alternative unique identifier of the image, otherwise grab the digest
256        let image_tag = match build_image
257            .repo_tags
258            .iter()
259            .find(|t| !t.ends_with("latest"))
260        {
261            Some(tag) => tag.to_owned(),
262            None => build_image.id.clone(),
263        };
264
265        metadata.image = Some(image_tag);
266
267        crate::metadata::write_metadata(metadata_artifacts, metadata, verbosity, true)?;
268    }
269    Ok(())
270}
271
272/// Searches for the local copy of the docker image.
273async fn find_local_image(
274    client: &Docker,
275    image: String,
276) -> Result<Option<ImageSummary>> {
277    let images = client
278        .list_images(Some(ListImagesOptions::<String> {
279            all: true,
280            ..Default::default()
281        }))
282        .await?;
283    let build_image = images.iter().find(|i| i.repo_tags.contains(&image));
284
285    Ok(build_image.cloned())
286}
287
288/// Creates the container, returning the container id if successful.
289///
290/// If the image is not available locally, it will be pulled from the registry.
291async fn create_container(
292    client: &Docker,
293    mut build_args: Vec<String>,
294    build_image: &str,
295    contract_name: &str,
296    host_folder: &Path,
297    verbosity: &Verbosity,
298) -> Result<String> {
299    let entrypoint = vec!["cargo".to_string(), "contract".to_string()];
300
301    let mut cmd = vec![
302        "build".to_string(),
303        "--release".to_string(),
304        "--output-json".to_string(),
305    ];
306
307    cmd.append(&mut build_args);
308
309    let digest_code = container_digest(cmd.clone(), build_image.to_string());
310    let container_name =
311        format!("ink-verified-{}-{}", contract_name, digest_code.clone());
312
313    let mut filters = HashMap::new();
314    filters.insert("name".to_string(), vec![container_name.clone()]);
315
316    let containers = client
317        .list_containers(Some(ListContainersOptions::<String> {
318            all: true,
319            filters,
320            ..Default::default()
321        }))
322        .await?;
323
324    let container_option = containers.first();
325
326    if container_option.is_some() {
327        return Ok(container_name)
328    }
329
330    let mount = Mount {
331        target: Some(String::from(MOUNT_DIR)),
332        source: Some(
333            host_folder
334                .to_str()
335                .context("Cannot convert path to string.")?
336                .to_string(),
337        ),
338        typ: Some(MountTypeEnum::BIND),
339        ..Default::default()
340    };
341    let host_cfg = Some(HostConfig {
342        mounts: Some(vec![mount]),
343        ..Default::default()
344    });
345
346    let user;
347    #[cfg(unix)]
348    {
349        user = Some(format!(
350            "{}:{}",
351            uzers::get_current_uid(),
352            uzers::get_current_gid()
353        ));
354    };
355    #[cfg(windows)]
356    {
357        user = None;
358    }
359
360    let config = Config {
361        image: Some(build_image.to_string()),
362        entrypoint: Some(entrypoint),
363        cmd: Some(cmd),
364        host_config: host_cfg,
365        attach_stderr: Some(true),
366        user,
367        ..Default::default()
368    };
369    let options = Some(CreateContainerOptions {
370        name: container_name.as_str(),
371        platform: Some("linux/amd64"),
372    });
373
374    match client
375        .create_container(options.clone(), config.clone())
376        .await
377    {
378        Ok(_) => Ok(container_name),
379        Err(err) => {
380            if matches!(
381                err,
382                bollard::errors::Error::DockerResponseServerError {
383                    status_code: 404,
384                    ..
385                }
386            ) {
387                // no such image locally, so pull and try again
388                pull_image(client, build_image.to_string(), verbosity).await?;
389                client
390                    .create_container(options, config)
391                    .await
392                    .context("Failed to create docker container")
393                    .map(|_| container_name)
394            } else {
395                Err(err.into())
396            }
397        }
398    }
399}
400
401/// Starts the container and executed the build inside it.
402async fn run_build(
403    client: &Docker,
404    container_name: &str,
405    verbosity: &Verbosity,
406) -> Result<BuildResult> {
407    client
408        .start_container::<String>(container_name, None)
409        .await?;
410
411    let AttachContainerResults { mut output, .. } = client
412        .attach_container(
413            container_name,
414            Some(AttachContainerOptions::<String> {
415                stdout: Some(true),
416                stderr: Some(true),
417                stream: Some(true),
418                ..Default::default()
419            }),
420        )
421        .await?;
422
423    verbose_eprintln!(
424        verbosity,
425        " {} {}",
426        "[==]".bold(),
427        format!("Started the build inside the container: {}", container_name)
428            .bright_cyan()
429            .bold(),
430    );
431
432    // pipe docker attach output into stdout
433    let stderr = std::io::stderr();
434    let mut stderr = stderr.lock();
435    let mut build_result = None;
436    let mut message_bytes: Vec<u8> = vec![];
437    while let Some(Ok(output)) = output.next().await {
438        match output {
439            LogOutput::StdOut { message } => {
440                message_bytes.extend(&message);
441            }
442            LogOutput::StdErr { message } => {
443                stderr.write_all(message.as_ref())?;
444                stderr.flush()?;
445            }
446            LogOutput::Console { message: _ } => {
447                panic!("LogOutput::Console")
448            }
449            LogOutput::StdIn { message: _ } => panic!("LogOutput::StdIn"),
450        };
451    }
452
453    if !message_bytes.is_empty() {
454        build_result = Some(
455            serde_json::from_reader(BufReader::new(message_bytes.as_slice())).context(
456                format!(
457                    "Error decoding BuildResult:\n {}",
458                    std::str::from_utf8(&message_bytes).unwrap()
459                ),
460            ),
461        )
462    };
463
464    if let Some(build_result) = build_result {
465        build_result
466    } else {
467        Err(anyhow::anyhow!(
468            "Failed to read build result from docker build"
469        ))
470    }
471}
472
473/// Takes CLI args from the host and appends them to the build command inside the docker.
474fn compose_build_args() -> Result<Vec<String>> {
475    use regex::Regex;
476    let mut args: Vec<String> = Vec::new();
477    // match `--image` or `verify` with arg with 1 or more white spaces surrounded
478    let rex = Regex::new(r#"(--image|verify)[ ]*[^ ]*[ ]*"#)?;
479    // we join the args together, so we can remove `--image <arg>`
480    let args_string: String = std::env::args().collect::<Vec<String>>().join(" ");
481    let args_string = rex.replace_all(&args_string, "").to_string();
482
483    // and then we turn it back to the vec, filtering out commands and arguments
484    // that should not be passed to the docker build command
485    let mut os_args: Vec<String> = args_string
486        .split_ascii_whitespace()
487        .filter(|a| {
488            a != &"--verifiable"
489                && !a.contains("cargo-contract")
490                && a != &"cargo"
491                && a != &"contract"
492                && a != &"build"
493                && a != &"--output-json"
494        })
495        .map(|s| s.to_string())
496        .collect();
497
498    args.append(&mut os_args);
499
500    Ok(args)
501}
502
503/// Pulls the docker image from the registry.
504async fn pull_image(client: &Docker, image: String, verbosity: &Verbosity) -> Result<()> {
505    let mut pull_image_stream = client.create_image(
506        Some(CreateImageOptions {
507            from_image: image,
508            ..Default::default()
509        }),
510        None,
511        None,
512    );
513
514    verbose_eprintln!(
515        verbosity,
516        " {} {}",
517        "[==]".bold(),
518        "Image does not exist. Pulling one from the registry"
519            .bright_cyan()
520            .bold()
521    );
522
523    if verbosity.is_verbose() {
524        show_pull_progress(pull_image_stream).await?
525    } else {
526        while pull_image_stream.next().await.is_some() {}
527    }
528
529    Ok(())
530}
531
532/// Display the progress of the pulling of each image layer.
533async fn show_pull_progress(
534    mut pull_image_stream: impl Stream<Item = Result<CreateImageInfo, Error>> + Sized + Unpin,
535) -> Result<()> {
536    use crossterm::{
537        cursor,
538        terminal::{
539            self,
540            ClearType,
541        },
542    };
543
544    let mut layers = Vec::new();
545    let mut curr_index = 0i16;
546    while let Some(result) = pull_image_stream.next().await {
547        let info = result?;
548
549        let status = info.status.unwrap_or_default();
550        if status.starts_with("Digest:") || status.starts_with("Status:") {
551            eprintln!("{}", status);
552            continue
553        }
554
555        if let Some(id) = info.id {
556            let mut move_cursor = String::new();
557            if let Some(index) = layers.iter().position(|l| l == &id) {
558                let index = index + 1;
559                let diff = index as i16 - curr_index;
560                curr_index = index as i16;
561                match diff.cmp(&1) {
562                    Ordering::Greater => {
563                        let down = diff - 1;
564                        move_cursor = format!("{}", cursor::MoveDown(down as u16))
565                    }
566                    Ordering::Less => {
567                        let up = diff.abs() + 1;
568                        move_cursor = format!("{}", cursor::MoveUp(up as u16))
569                    }
570                    Ordering::Equal => {}
571                }
572            } else {
573                layers.push(id.clone());
574                let len = layers.len() as i16;
575                let diff = len - curr_index;
576                curr_index = len;
577                if diff > 1 {
578                    move_cursor = format!("{}", cursor::MoveDown(diff as u16))
579                }
580            };
581
582            let clear_line = terminal::Clear(ClearType::CurrentLine);
583
584            if status == "Pull complete" {
585                eprintln!("{}{}{}: {}", move_cursor, clear_line, id, status)
586            } else {
587                let progress = info.progress.unwrap_or_default();
588                eprintln!(
589                    "{}{}{}: {} {}",
590                    move_cursor, clear_line, id, status, progress
591                )
592            }
593        }
594    }
595    Ok(())
596}
597
598/// Calculates the unique container's code.
599fn container_digest(entrypoint: Vec<String>, image_digest: String) -> String {
600    // in order to optimise the container usage
601    // we are hashing the inputted command
602    // in order to reuse the container for the same permutation of arguments
603    let mut s = DefaultHasher::new();
604    // the data is set of commands and args and the image digest
605    let data = (entrypoint, image_digest);
606    data.hash(&mut s);
607    let digest = s.finish();
608    // taking the first 5 digits to be a unique identifier
609    let digest_code: String = digest.to_string().chars().take(5).collect();
610    digest_code
611}