Skip to main content

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