bake 0.6.0

A containerized build system.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
use crate::format::CodeStr;
use indicatif::{ProgressBar, ProgressStyle};
use std::{
  fs::{create_dir_all, metadata, rename},
  io,
  io::Read,
  path::{Path, PathBuf},
  process::{ChildStdin, Command, Stdio},
  sync::{
    atomic::{AtomicBool, Ordering},
    Arc,
  },
  thread,
  thread::sleep,
  time::Duration,
};
use tempfile::tempdir;
use walkdir::WalkDir;

// Query whether an image exists locally.
pub fn image_exists(
  image: &str,
  running: &Arc<AtomicBool>,
) -> Result<bool, String> {
  debug!("Checking existence of image {}\u{2026}", image.code_str());
  if let Err(e) = run_quiet(
    &["image", "inspect", image],
    "The image doesn't exist.",
    running,
  ) {
    if running.load(Ordering::SeqCst) {
      Ok(false)
    } else {
      Err(e)
    }
  } else {
    Ok(true)
  }
}

// Push an image.
pub fn push_image(
  image: &str,
  running: &Arc<AtomicBool>,
) -> Result<(), String> {
  debug!("Pushing image {}\u{2026}", image.code_str());
  run_quiet(&["image", "push", image], "Unable to push image.", running)
    .map(|_| ())
}

// Pull an image.
pub fn pull_image(
  image: &str,
  running: &Arc<AtomicBool>,
) -> Result<(), String> {
  debug!("Pulling image {}\u{2026}", image.code_str());
  run_quiet(&["image", "pull", image], "Unable to pull image.", running)
    .map(|_| ())
}

// Delete an image.
pub fn delete_image(
  image: &str,
  running: &Arc<AtomicBool>,
) -> Result<(), String> {
  debug!("Deleting image {}\u{2026}", image.code_str());
  run_quiet(
    &["image", "rm", "--force", image],
    "Unable to delete image.",
    running,
  )
  .map(|_| ())
}

// Create a container and return its ID.
pub fn create_container(
  image: &str,
  command: &str,
  running: &Arc<AtomicBool>,
) -> Result<String, String> {
  debug!(
    "Creating container from image {} with command {}\u{2026}",
    image.code_str(),
    command.code_str()
  );

  // Why `--init`? (1) PID 1 is supposed to reap orphaned zombie processes,
  // otherwise they can accumulate. Bash does this, but we run `/bin/sh` in the
  // container, which may or may not be Bash. So `--init` runs Tini
  // (https://github.com/krallin/tini) as PID 1, which properly reaps orphaned
  // zombies. (2) PID 1 also does not exhibit the default behavior (crashing)
  // for signals like SIGINT and SIGTERM. However, PID 1 can still handle these
  // signals by explicitly trapping them. Tini traps these signals and forwards
  // them to the child process. Then the default signal handling behavior of
  // the child process (in our case, `/bin/sh`) works normally. [tag:--init]
  Ok(
    run_quiet(
      vec![
        "container",
        "create",
        "--init",
        image,
        "/bin/sh",
        "-c",
        command,
      ]
      .as_ref(),
      "Unable to create container.",
      running,
    )?
    .trim()
    .to_owned(),
  )
}

// Copy files into a container.
pub fn copy_into_container<R: Read>(
  container: &str,
  mut tar: R,
  running: &Arc<AtomicBool>,
) -> Result<(), String> {
  debug!(
    "Copying files into container {}\u{2026}",
    container.code_str()
  );
  run_quiet_stdin(
    &["container", "cp", "-", &format!("{}:{}", container, "/")],
    "Unable to copy files into the container.",
    |mut stdin| {
      io::copy(&mut tar, &mut stdin).map_err(|e| {
        format!("Unable to copy files into the container.. Details: {}", e)
      })?;

      Ok(())
    },
    running,
  )
  .map(|_| ())
}

// Copy files from a container.
pub fn copy_from_container(
  container: &str,
  paths: &[PathBuf],
  source_dir: &Path,
  destination_dir: &Path,
  running: &Arc<AtomicBool>,
) -> Result<(), String> {
  // Copy each path from the container to the host.
  for path in paths {
    debug!(
      "Copying `{}` from container {}\u{2026}",
      path.to_string_lossy(),
      container.code_str()
    );

    // `docker container cp` is not idempotent. For example, suppose there is a
    // directory called `/foo` in the container and `/bar` does not exist on
    // the host. Consider the following command:
    //   `docker cp container:/foo /bar`
    // The first time that command is run, Docker will create the directory
    // `/bar` on the host and copy the files from `/foo` into it. But if you
    // run it again, Docker will copy `/bar` into the directory `/foo`,
    // resulting in `/foo/foo`, which is undesirable. To work around this, we
    // first copy the path from the container into a temporary directory (where
    // the target path is guaranteed to not exist). Then we copy/move that to
    // the final destination.
    let temp_dir = tempdir().map_err(|e| {
      format!("Unable to create temporary directory. Details: {}", e)
    })?;

    // Figure out what needs to go where.
    let source = source_dir.join(path);
    let intermediate = temp_dir.path().join("data");
    let destination = destination_dir.join(path);

    // Get the path from the container.
    run_quiet(
      &[
        "container",
        "cp",
        &format!("{}:{}", container, source.to_string_lossy()),
        &intermediate.to_string_lossy(),
      ],
      "Unable to copy files from the container.",
      running,
    )
    .map(|_| ())?;

    // Check if what we got from the container is a file or a directory.
    let metadata_err_map = |e| {
      format!(
        "Unable to retrieve filesystem metadata for path {}. Details: {}",
        intermediate.to_string_lossy().code_str(),
        e
      )
    };
    if metadata(&intermediate).map_err(metadata_err_map)?.is_file() {
      // It's a file. Just move it to the destination.
      rename(&intermediate, &destination).map_err(|e| {
        format!(
          "Unable to move file {} to destination {}. Details: {}",
          intermediate.to_string_lossy().code_str(),
          destination.to_string_lossy().code_str(),
          e
        )
      })?;
    } else {
      // It's a directory. Traverse it.
      for entry in WalkDir::new(&intermediate) {
        // If we run into an error traversing the filesystem, report it.
        let entry = entry.map_err(|e| {
          format!(
            "Unable to traverse directory {}. Details: {}",
            intermediate.to_string_lossy().code_str(),
            e
          )
        })?;

        // Figure out what needs to go where. The `unwrap` is safe because
        // `entry` is guaranteed to be inside `intermediate` (or equal to it).
        let entry_path = entry.path();
        let destination_path =
          destination.join(entry_path.strip_prefix(&intermediate).unwrap());

        // Check if the current entry is a file or a directory.
        if entry.file_type().is_dir() {
          // It's a directory. Create a directory at the destination.
          create_dir_all(&destination_path).map_err(|e| {
            format!(
              "Unable to create directory {}. Details: {}",
              destination_path.to_string_lossy().code_str(),
              e
            )
          })?;
        } else {
          // It's a file. Move it to the destination.
          rename(entry_path, &destination_path).map_err(|e| {
            format!(
              "Unable to move file {} to destination {}. Details: {}",
              entry_path.to_string_lossy().code_str(),
              destination_path.to_string_lossy().code_str(),
              e
            )
          })?;
        }
      }
    }
  }

  Ok(())
}

// Start a container.
pub fn start_container(
  container: &str,
  running: &Arc<AtomicBool>,
) -> Result<(), String> {
  debug!("Starting container {}\u{2026}", container.code_str());
  run_loud(
    &["container", "start", "--attach", container],
    "Unable to start container.",
    running,
  )
  .map(|_| ())
}

// Stop a container.
pub fn stop_container(
  container: &str,
  running: &Arc<AtomicBool>,
) -> Result<(), String> {
  debug!("Stopping container {}\u{2026}", container.code_str());
  run_quiet(
    &["container", "stop", container],
    "Unable to stop container.",
    running,
  )
  .map(|_| ())
}

// Commit a container to an image.
pub fn commit_container(
  container: &str,
  image: &str,
  running: &Arc<AtomicBool>,
) -> Result<(), String> {
  debug!(
    "Committing container {} to image {}\u{2026}",
    container.code_str(),
    image.code_str()
  );
  run_quiet(
    &["container", "commit", container, image],
    "Unable to commit container.",
    running,
  )
  .map(|_| ())
}

// Delete a container.
pub fn delete_container(
  container: &str,
  running: &Arc<AtomicBool>,
) -> Result<(), String> {
  debug!("Deleting container {}\u{2026}", container.code_str());
  run_quiet(
    &["container", "rm", "--force", container],
    "Unable to delete container.",
    running,
  )
  .map(|_| ())
}

// Run an interactive shell.
pub fn spawn_shell(
  image: &str,
  running: &Arc<AtomicBool>,
) -> Result<(), String> {
  debug!(
    "Spawning an interactive shell for image {}\u{2026}",
    image.code_str()
  );
  run_attach(
    &[
      "container",
      "run",
      "--rm",
      "--interactive",
      "--tty",
      "--init", // [ref:--init]
      image,
      "/bin/su", // We use `su` rather than `sh` to use the root user's shell.
      "-l",
    ],
    "The shell exited with a failure.",
    running,
  )
}

// Run a command and return its standard output.
fn run_quiet(
  args: &[&str],
  error: &str,
  running: &Arc<AtomicBool>,
) -> Result<String, String> {
  let stop_spinning = spin();
  defer! {{
    stop_spinning();
  }}

  let output = command(args)
    .stdin(Stdio::null())
    .output()
    .map_err(|e| format!("{}\nDetails: {}", error, e))?;

  if output.status.success() {
    Ok(String::from_utf8_lossy(&output.stdout).to_string())
  } else {
    Err(if running.load(Ordering::SeqCst) {
      format!(
        "{}\nDetails: {}",
        error,
        String::from_utf8_lossy(&output.stderr)
      )
    } else {
      super::INTERRUPT_MESSAGE.to_owned()
    })
  }
}

// Run a command and return its standard output. Accepts a closure which
// receives a pipe to the standard input stream of the child process.
fn run_quiet_stdin<W: FnOnce(&mut ChildStdin) -> Result<(), String>>(
  args: &[&str],
  error: &str,
  writer: W,
  running: &Arc<AtomicBool>,
) -> Result<String, String> {
  let stop_spinning = spin();
  defer! {{
    stop_spinning();
  }}

  let mut child = command(args)
    .stdin(Stdio::piped()) // [tag:stdin_piped]
    .stdout(Stdio::piped())
    .stderr(Stdio::piped())
    .spawn()
    .map_err(|e| format!("{}\nDetails: {}", error, e))?;
  writer(child.stdin.as_mut().unwrap())?; // [ref:stdin_piped]
  let output = child
    .wait_with_output()
    .map_err(|e| format!("{}\nDetails: {}", error, e))?;

  if output.status.success() {
    Ok(String::from_utf8_lossy(&output.stdout).to_string())
  } else {
    Err(if running.load(Ordering::SeqCst) {
      format!(
        "{}\nDetails: {}",
        error,
        String::from_utf8_lossy(&output.stderr)
      )
    } else {
      super::INTERRUPT_MESSAGE.to_owned()
    })
  }
}

// Run a command and forward its standard output and error streams.
fn run_loud(
  args: &[&str],
  error: &str,
  running: &Arc<AtomicBool>,
) -> Result<(), String> {
  let status = command(args)
    .stdin(Stdio::null())
    .status()
    .map_err(|e| format!("{}\nDetails: {}", error, e))?;
  if status.success() {
    Ok(())
  } else {
    Err(
      if running.load(Ordering::SeqCst) {
        error
      } else {
        super::INTERRUPT_MESSAGE
      }
      .to_owned(),
    )
  }
}

// Run a command and forward its standard input, output, and error streams.
fn run_attach(
  args: &[&str],
  error: &str,
  running: &Arc<AtomicBool>,
) -> Result<(), String> {
  let status = command(args)
    .status()
    .map_err(|e| format!("{}\nDetails: {}", error, e))?;
  if status.success() {
    Ok(())
  } else {
    Err(
      if running.load(Ordering::SeqCst) {
        error
      } else {
        super::INTERRUPT_MESSAGE
      }
      .to_owned(),
    )
  }
}

// Construct a Docker `Command` from an array of arguments.
fn command(args: &[&str]) -> Command {
  let mut command = Command::new("docker");
  for arg in args {
    command.arg(arg);
  }
  command
}

// Render a spinner in the terminal and return a closure to kill it.
fn spin() -> impl FnOnce() {
  let spinning = Arc::new(AtomicBool::new(true));
  let spinning_clone = spinning.clone();

  let child = thread::spawn(move || {
    let spinner = ProgressBar::new(1);
    spinner.set_style(ProgressStyle::default_spinner());
    while spinning_clone.load(Ordering::SeqCst) {
      spinner.tick();
      sleep(Duration::from_millis(100));
    }
    spinner.finish_and_clear();
  });

  move || {
    spinning.store(false, Ordering::SeqCst);
    let _ = child.join();
  }
}