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