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
// Build method selection and configuration
use anyhow::{bail, Result};
use clap::Args;
use std::path::Path;
use tracing::info;
use crate::config::{Config, ContainerCli};
/// Build method for container images
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum BuildMethod {
Docker {
use_buildx: bool,
},
Pack,
Railpack {
use_buildctl: bool,
},
/// Plain buildctl with Dockerfile (without railpack)
Buildctl,
}
/// Build-related CLI arguments that can be flattened into command structs
#[derive(Debug, Clone, Args)]
pub struct BuildArgs {
/// Build backend (docker[:build|:buildx|:buildctl], pack, railpack[:buildx|:buildctl])
#[arg(long)]
pub backend: Option<String>,
/// Buildpack builder to use (only for pack backend)
#[arg(long)]
pub builder: Option<String>,
/// Buildpack(s) to use (only for pack backend). Can be specified multiple times.
#[arg(long = "buildpack", short = 'b')]
pub buildpacks: Vec<String>,
/// Build-time environment variables (for build configuration only).
/// Format: KEY=VALUE (with explicit value) or KEY (reads from current environment).
///
/// Examples:
/// -e NODE_ENV=production -e API_VERSION=1.2.3
/// -e DATABASE_URL (reads DATABASE_URL from current environment)
///
/// Can also be configured in rise.toml under [build] section.
/// CLI values are merged with rise.toml values.
///
/// WARNING: Build-time variables are for build configuration (compiler flags,
/// tool versions, feature toggles), NOT runtime secrets. For runtime secrets,
/// use 'rise env set --secret' instead.
#[arg(long = "env", short = 'e', value_name = "KEY=VALUE")]
pub env: Vec<String>,
/// Container CLI to use (docker or podman)
#[arg(long)]
pub container_cli: Option<String>,
/// Enable managed BuildKit daemon with SSL certificate support
#[arg(long, value_parser = clap::value_parser!(bool), default_missing_value = "true", num_args = 0..=1)]
pub managed_buildkit: Option<bool>,
/// Path to Dockerfile (relative to app path / rise.toml location). Defaults to "Dockerfile" or "Containerfile"
#[arg(long)]
pub dockerfile: Option<String>,
/// Default build context (docker/podman only) - the context directory for the build.
/// This is the path argument to `docker build <path>`. Defaults to app path.
/// Path is relative to the app path / rise.toml location.
#[arg(long = "context")]
pub build_context: Option<String>,
/// Build contexts for multi-stage builds (docker/podman only). Can be specified multiple times.
/// Format: name=path where path is relative to app path / rise.toml location
#[arg(long = "build-context")]
pub build_contexts: Vec<String>,
/// Disable build cache (equivalent to docker build --no-cache, pack build --clear-cache)
#[arg(long)]
pub no_cache: bool,
}
/// Options for building container images
#[derive(Debug, Clone)]
pub(crate) struct BuildOptions {
pub image_tag: String,
pub app_path: String,
pub backend: Option<String>,
pub builder: Option<String>,
pub buildpacks: Vec<String>,
pub env: Vec<String>,
pub container_cli: ContainerCli,
/// Whether --container-cli was explicitly provided (for "ignored" warnings)
pub explicit_container_cli: bool,
/// None = auto-detect based on SSL_CERT_FILE and BUILDKIT_HOST
/// Some(true) = explicitly enable managed buildkit
/// Some(false) = explicitly disable managed buildkit
pub managed_buildkit: Option<bool>,
pub push: bool,
/// Path to Dockerfile (relative to app_path / rise.toml location)
pub dockerfile: Option<String>,
/// Default build context (relative to app_path / rise.toml location)
/// This is the path argument to `docker build <path>`. Defaults to app_path if None.
/// Note: This value is resolved to an absolute path in build_image() before use.
pub build_context: Option<String>,
/// Build contexts for multi-stage builds (docker/podman only)
/// Format: name -> path (relative to app_path / rise.toml location)
/// Note: These paths are resolved to absolute paths in build_image() before use.
pub build_contexts: std::collections::HashMap<String, String>,
/// Disable build cache
pub no_cache: bool,
}
impl BuildOptions {
/// Create BuildOptions from BuildArgs and Config
///
/// Configuration precedence (highest to lowest):
/// 1. CLI flags (BuildArgs)
/// 2. Environment variables (RISE_*)
/// 3. Project config file (rise.toml / .rise.toml)
/// 4. Global config file (via Config getters)
/// 5. Auto-detection/defaults (via Config getters)
pub(crate) fn from_build_args(
config: &Config,
image_tag: String,
app_path: String,
build_args: &BuildArgs,
) -> Self {
use tracing::warn;
// Load project-level build config from app_path with error handling
let project_config = match crate::build::config::load_full_project_config(&app_path) {
Ok(cfg) => cfg.and_then(|c| c.build),
Err(e) => {
warn!(
"Failed to load project config: {:#}. Continuing without it.",
e
);
None
}
};
// Merge: CLI > Project > Environment (via Config) > Global (via Config) > Defaults
Self {
image_tag,
app_path,
// String options - use first non-None value
backend: build_args
.backend
.clone()
.or_else(|| project_config.as_ref().and_then(|c| c.backend.clone())),
builder: build_args
.builder
.clone()
.or_else(|| project_config.as_ref().and_then(|c| c.builder.clone())),
container_cli: {
let explicit = build_args
.container_cli
.clone()
.or_else(|| crate::build::env_var_non_empty("RISE_CONTAINER_CLI"))
.or_else(|| {
project_config
.as_ref()
.and_then(|c| c.container_cli.clone())
});
match explicit {
Some(name) => ContainerCli::from_command(name),
None => config.get_container_cli(),
}
},
explicit_container_cli: build_args.container_cli.is_some(),
// Vector options - all vectors merge config + CLI values (append)
buildpacks: {
let mut packs = project_config
.as_ref()
.and_then(|c| c.buildpacks.clone())
.unwrap_or_default();
packs.extend(build_args.buildpacks.clone());
packs
},
env: {
let mut env = project_config
.as_ref()
.and_then(|c| c.env.clone())
.unwrap_or_default();
env.extend(build_args.env.clone());
env
},
// Boolean options: CLI flag > env var > project config > global config > auto-detect
managed_buildkit: build_args
.managed_buildkit
.or_else(|| crate::build::parse_bool_env_var("RISE_MANAGED_BUILDKIT"))
.or_else(|| project_config.as_ref().and_then(|c| c.managed_buildkit))
.or(config.managed_buildkit),
dockerfile: build_args
.dockerfile
.clone()
.or_else(|| project_config.as_ref().and_then(|c| c.dockerfile.clone())),
build_context: build_args.build_context.clone().or_else(|| {
project_config
.as_ref()
.and_then(|c| c.build_context.clone())
}),
// Build contexts - merge config + CLI values (CLI overrides config for same name)
build_contexts: {
let mut contexts = project_config
.as_ref()
.and_then(|c| c.build_contexts.clone())
.unwrap_or_default();
// Parse and merge CLI build contexts (format: "name=path")
for ctx in &build_args.build_contexts {
if let Some((name, path)) = ctx.split_once('=') {
contexts.insert(name.to_string(), path.to_string());
} else {
warn!(
"Invalid build context format '{}'. Expected 'name=path'. Ignoring.",
ctx
);
}
}
contexts
},
no_cache: build_args.no_cache
|| project_config
.as_ref()
.and_then(|c| c.no_cache)
.unwrap_or(false),
push: false,
}
}
/// Builder method to set push flag
pub(crate) fn with_push(mut self, push: bool) -> Self {
self.push = push;
self
}
}
impl BuildMethod {
/// Parse backend string into BuildMethod
pub(crate) fn from_backend_str(backend: &str) -> Result<Self> {
match backend {
"docker" | "docker:build" => Ok(BuildMethod::Docker { use_buildx: false }),
"docker:buildx" => Ok(BuildMethod::Docker { use_buildx: true }),
"buildctl" | "docker:buildctl" => Ok(BuildMethod::Buildctl),
"pack" => Ok(BuildMethod::Pack),
"railpack" | "railpack:buildx" => Ok(BuildMethod::Railpack {
use_buildctl: false,
}),
"railpack:buildctl" => Ok(BuildMethod::Railpack { use_buildctl: true }),
_ => bail!(
"Invalid build backend '{}'. Supported: docker, docker:build, docker:buildx, buildctl, docker:buildctl, pack, railpack, railpack:buildctl",
backend
),
}
}
}
/// Select build method based on explicit backend or auto-detection
/// Returns (BuildMethod, Option<dockerfile_path>)
pub(crate) fn select_build_method(
app_path: &str,
backend: Option<&str>,
dockerfile: Option<&str>,
container_cli: &str,
) -> Result<(BuildMethod, Option<String>)> {
// Determine dockerfile path
let (dockerfile_path, dockerfile_relative) = if let Some(df) = dockerfile {
let path = Path::new(app_path).join(df);
(path, Some(df.to_string()))
} else {
// Auto-detect: Dockerfile first, then Containerfile
let dockerfile = Path::new(app_path).join("Dockerfile");
let containerfile = Path::new(app_path).join("Containerfile");
if dockerfile.exists() && dockerfile.is_file() {
(dockerfile, Some("Dockerfile".to_string()))
} else if containerfile.exists() && containerfile.is_file() {
info!("Detected Containerfile");
(containerfile, Some("Containerfile".to_string()))
} else {
// No dockerfile found
(Path::new(app_path).join("Dockerfile"), None)
}
};
if let Some(backend_str) = backend {
// Explicit backend specified
let method = BuildMethod::from_backend_str(backend_str)?;
Ok((method, dockerfile_relative))
} else {
// Auto-detect based on dockerfile presence
if dockerfile_path.exists() && dockerfile_path.is_file() {
// Check if buildx is available
let use_buildx = super::docker::is_buildx_available(container_cli);
if use_buildx {
info!(
"Detected {}, using docker:buildx backend",
dockerfile_relative.as_deref().unwrap_or("Dockerfile")
);
} else {
info!(
"Detected {}, using docker backend",
dockerfile_relative.as_deref().unwrap_or("Dockerfile")
);
}
Ok((BuildMethod::Docker { use_buildx }, dockerfile_relative))
} else {
info!("No Dockerfile found, using railpack backend");
Ok((
BuildMethod::Railpack {
use_buildctl: false,
},
None,
))
}
}
}
/// Check if a build method requires BuildKit
pub(crate) fn requires_buildkit(method: &BuildMethod) -> bool {
matches!(
method,
BuildMethod::Docker { use_buildx: true }
| BuildMethod::Railpack { .. }
| BuildMethod::Buildctl
)
}