1use 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;
96const IMAGE: &str = "useink/contracts-verifiable";
98const VERSION: &str = env!("CARGO_PKG_VERSION");
100const MOUNT_DIR: &str = "/contract";
102
103#[derive(Clone, Debug, Default)]
105pub enum ImageVariant {
106 #[default]
108 Default,
109 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
123pub 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 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
202fn 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 #[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
242async 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 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
272async 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
288async 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 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
401async 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 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
473fn compose_build_args() -> Result<Vec<String>> {
475 use regex::Regex;
476 let mut args: Vec<String> = Vec::new();
477 let rex = Regex::new(r#"(--image|verify)[ ]*[^ ]*[ ]*"#)?;
479 let args_string: String = std::env::args().collect::<Vec<String>>().join(" ");
481 let args_string = rex.replace_all(&args_string, "").to_string();
482
483 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
503async 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
532async 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
598fn container_digest(entrypoint: Vec<String>, image_digest: String) -> String {
600 let mut s = DefaultHasher::new();
604 let data = (entrypoint, image_digest);
606 data.hash(&mut s);
607 let digest = s.finish();
608 let digest_code: String = digest.to_string().chars().take(5).collect();
610 digest_code
611}