1use 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;
98const IMAGE: &str = "useink/contracts-verifiable";
100const VERSION: &str = env!("CARGO_PKG_VERSION");
102const MOUNT_DIR: &str = "/contract";
104
105#[derive(Clone, Debug, Default)]
107pub enum ImageVariant {
108 #[default]
110 Default,
111 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 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 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
205fn 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 #[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
257async 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 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
310async 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
326async 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 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
439async 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 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
514fn compose_build_args() -> Result<Vec<String>> {
516 use regex::Regex;
517 let mut args: Vec<String> = Vec::new();
518 let rex = Regex::new(r#"(--image|verify)[ ]*[^ ]*[ ]*"#)?;
520 let args_string: String = std::env::args().collect::<Vec<String>>().join(" ");
522 let args_string = rex.replace_all(&args_string, "").to_string();
523
524 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
544async 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
573async 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
636fn container_digest(entrypoint: Vec<String>, image_digest: String) -> String {
638 let mut s = DefaultHasher::new();
642 let data = (entrypoint, image_digest);
644 data.hash(&mut s);
645 let digest = s.finish();
646 let digest_code: String = digest.to_string().chars().take(5).collect();
648 digest_code
649}