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 trait ComposeBuildArgs {
126 fn compose_build_args() -> Result<Vec<String>>;
129}
130
131pub 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 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
211fn 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 #[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
263async 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 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
316async 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
332async 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 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
445async 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 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
520async 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
549async 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
612fn container_digest(entrypoint: Vec<String>, image_digest: String) -> String {
614 let mut s = DefaultHasher::new();
618 let data = (entrypoint, image_digest);
620 data.hash(&mut s);
621 let digest = s.finish();
622 let digest_code: String = digest.to_string().chars().take(5).collect();
624 digest_code
625}