summaryrefslogtreecommitdiff
path: root/src/build.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/build.rs')
-rw-r--r--src/build.rs131
1 files changed, 73 insertions, 58 deletions
diff --git a/src/build.rs b/src/build.rs
index e887f64..b56e680 100644
--- a/src/build.rs
+++ b/src/build.rs
@@ -1,11 +1,11 @@
use anyhow::{Context as _, Result};
+use log::{debug, info};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::time::{Duration, Instant};
use tokio::io::{AsyncWrite, AsyncWriteExt as _, BufWriter};
use tokio::process::Command;
-use tracing::{debug, info};
use crate::repo_config::RepoConfig;
@@ -82,13 +82,13 @@ impl std::error::Error for BuildFailure {}
///
/// Used for `--verbose` mode: streams build output to both a temp file (primary)
/// and stderr (secondary) simultaneously.
-pub(crate) struct TeeWriter<W> {
+pub struct TeeWriter<W> {
primary: W,
secondary: tokio::io::Stderr,
}
impl<W: AsyncWrite + Unpin> TeeWriter<W> {
- pub(crate) const fn new(primary: W, secondary: tokio::io::Stderr) -> Self {
+ pub const fn new(primary: W, secondary: tokio::io::Stderr) -> Self {
Self { primary, secondary }
}
}
@@ -125,56 +125,18 @@ impl<W: AsyncWrite + Unpin> AsyncWrite for TeeWriter<W> {
}
}
-/// Execute a containerized build for a site.
-///
-/// Stdout and stderr are streamed to the provided temporary files on disk
-/// instead of being buffered in memory. This removes unbounded memory usage
-/// for container builds.
-///
-/// # Arguments
-/// * `runtime` - Container runtime to use ("podman" or "docker")
-/// * `clone_dir` - Path to the cloned repository
-/// * `repo_config` - Build configuration from witryna.yaml
-/// * `cache_volumes` - Pairs of (`container_path`, `host_path`) for persistent cache mounts
-/// * `env` - User-defined environment variables to pass into the container via `--env`
-/// * `options` - Optional container resource limits and network mode
-/// * `stdout_file` - Temp file path for captured stdout
-/// * `stderr_file` - Temp file path for captured stderr
-/// * `timeout` - Maximum duration before killing the build
-/// * `verbose` - When true, also stream build output to stderr in real-time
-///
-/// # Errors
-///
-/// Returns an error if the container command times out, fails to execute,
-/// or exits with a non-zero status code (as a [`BuildFailure`]).
+/// Build the container CLI arguments for a build invocation.
///
-/// # Security
-/// - Uses typed arguments (no shell interpolation) per OWASP guidelines
-/// - Mounts clone directory as read-write (needed for build output)
-/// - Runs with minimal capabilities
-#[allow(clippy::implicit_hasher, clippy::too_many_arguments)]
-pub async fn execute(
+/// Assembles the full `run --rm ...` argument list including volume mounts,
+/// environment variables, resource limits, and the build command.
+fn build_container_args(
runtime: &str,
clone_dir: &Path,
repo_config: &RepoConfig,
cache_volumes: &[(String, PathBuf)],
env: &HashMap<String, String>,
options: &ContainerOptions,
- stdout_file: &Path,
- stderr_file: &Path,
- timeout: Duration,
- verbose: bool,
-) -> Result<BuildResult> {
- info!(
- image = %repo_config.image,
- command = %repo_config.command,
- path = %clone_dir.display(),
- "executing container build"
- );
-
- let start = Instant::now();
-
- // Build args dynamically to support optional cache volumes
+) -> Vec<String> {
let mut args = vec![
"run".to_owned(),
"--rm".to_owned(),
@@ -182,13 +144,13 @@ pub async fn execute(
format!("{}:/workspace:Z", clone_dir.display()),
];
- // Add cache volume mounts
+ // Cache volume mounts
for (container_path, host_path) in cache_volumes {
args.push("--volume".to_owned());
args.push(format!("{}:{}:Z", host_path.display(), container_path));
}
- // Add user-defined environment variables
+ // User-defined environment variables
for (key, value) in env {
args.push("--env".to_owned());
args.push(format!("{key}={value}"));
@@ -203,9 +165,6 @@ pub async fn execute(
if runtime == "podman" {
args.push("--userns=keep-id".to_owned());
} else {
- // Docker: container runs as root but workspace is owned by host UID.
- // DAC_OVERRIDE lets root bypass file permission checks.
- // Podman doesn't need this because --userns=keep-id maps to the host UID.
args.push("--cap-add=DAC_OVERRIDE".to_owned());
}
@@ -233,6 +192,59 @@ pub async fn execute(
repo_config.command.clone(),
]);
+ args
+}
+
+/// Execute a containerized build for a site.
+///
+/// Stdout and stderr are streamed to the provided temporary files on disk
+/// instead of being buffered in memory. This removes unbounded memory usage
+/// for container builds.
+///
+/// # Arguments
+/// * `runtime` - Container runtime to use ("podman" or "docker")
+/// * `clone_dir` - Path to the cloned repository
+/// * `repo_config` - Build configuration from witryna.yaml
+/// * `cache_volumes` - Pairs of (`container_path`, `host_path`) for persistent cache mounts
+/// * `env` - User-defined environment variables to pass into the container via `--env`
+/// * `options` - Optional container resource limits and network mode
+/// * `stdout_file` - Temp file path for captured stdout
+/// * `stderr_file` - Temp file path for captured stderr
+/// * `timeout` - Maximum duration before killing the build
+/// * `verbose` - When true, also stream build output to stderr in real-time
+///
+/// # Errors
+///
+/// Returns an error if the container command times out, fails to execute,
+/// or exits with a non-zero status code (as a [`BuildFailure`]).
+///
+/// # Security
+/// - Uses typed arguments (no shell interpolation) per OWASP guidelines
+/// - Mounts clone directory as read-write (needed for build output)
+/// - Runs with minimal capabilities
+#[allow(clippy::implicit_hasher, clippy::too_many_arguments)]
+pub async fn execute(
+ runtime: &str,
+ clone_dir: &Path,
+ repo_config: &RepoConfig,
+ cache_volumes: &[(String, PathBuf)],
+ env: &HashMap<String, String>,
+ options: &ContainerOptions,
+ stdout_file: &Path,
+ stderr_file: &Path,
+ timeout: Duration,
+ verbose: bool,
+) -> Result<BuildResult> {
+ info!(
+ "executing container build: image={} command={} path={}",
+ repo_config.image,
+ repo_config.command,
+ clone_dir.display()
+ );
+
+ let start = Instant::now();
+ let args = build_container_args(runtime, clone_dir, repo_config, cache_volumes, env, options);
+
// Spawn with piped stdout/stderr for streaming (OWASP: no shell interpolation)
let mut child = Command::new(runtime)
.args(&args)
@@ -265,7 +277,7 @@ pub async fn execute(
if verbose {
let mut stdout_tee = TeeWriter::new(stdout_file_writer, tokio::io::stderr());
let mut stderr_tee = TeeWriter::new(stderr_file_writer, tokio::io::stderr());
- run_build_process(
+ Box::pin(run_build_process(
child,
stdout_pipe,
stderr_pipe,
@@ -277,12 +289,12 @@ pub async fn execute(
clone_dir,
"container",
timeout,
- )
+ ))
.await
} else {
let mut stdout_writer = stdout_file_writer;
let mut stderr_writer = stderr_file_writer;
- run_build_process(
+ Box::pin(run_build_process(
child,
stdout_pipe,
stderr_pipe,
@@ -294,7 +306,7 @@ pub async fn execute(
clone_dir,
"container",
timeout,
- )
+ ))
.await
}
}
@@ -307,7 +319,7 @@ pub async fn execute(
/// meaningful error message in `BuildFailure::Display` without reading
/// the entire stderr file back into memory.
#[allow(clippy::indexing_slicing)] // buf[..n] bounded by read() return value
-pub(crate) async fn copy_with_tail<R, W>(
+pub async fn copy_with_tail<R, W>(
mut reader: R,
mut writer: W,
tail_size: usize,
@@ -386,7 +398,7 @@ where
if !status.success() {
let exit_code = status.code().unwrap_or(-1);
- debug!(exit_code, "{label} build failed");
+ debug!("{label} build failed: exit_code={exit_code}");
return Err(BuildFailure {
exit_code,
stdout_file: stdout_file.to_path_buf(),
@@ -398,7 +410,10 @@ where
}
let duration = start.elapsed();
- debug!(path = %clone_dir.display(), ?duration, "{label} build completed");
+ debug!(
+ "{label} build completed: path={} duration={duration:?}",
+ clone_dir.display()
+ );
Ok(BuildResult {
stdout_file: stdout_file.to_path_buf(),
stderr_file: stderr_file.to_path_buf(),