Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 75 additions & 2 deletions crates/dev_container/src/devcontainer_manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2301,10 +2301,10 @@ RUN sed -i -E 's/((^|\s)PATH=)([^\$]*)$/\1\${PATH:-\3}/g' /etc/profile || true
let mut parsed_line = line.to_string();
// Replace from devcontainer args first, since they take precedence
for (key, value) in &devcontainer_args {
parsed_line = parsed_line.replace(&format!("${{{key}}}"), value)
parsed_line = expand_dockerfile_var(parsed_line, key, value);
}
for (key, value) in &inline_args {
parsed_line = parsed_line.replace(&format!("${{{key}}}"), value);
parsed_line = expand_dockerfile_var(parsed_line, key, value);
}
if let Some(arg_directives) = parsed_line.strip_prefix("ARG ") {
let trimmed = arg_directives.trim();
Expand Down Expand Up @@ -2442,6 +2442,30 @@ enum DevContainerBuildResources {
Docker(DockerBuildResources),
}

/// Replaces occurrences of `${KEY}` and `$KEY` in `line` with `value`.
/// Bare `$KEY` is only replaced when the character immediately after the key
/// is not a word character (`[A-Za-z0-9_]`), so `$RUBY_VERSION2` is not
/// partially consumed when expanding `$RUBY_VERSION`.
fn expand_dockerfile_var(mut line: String, key: &str, value: &str) -> String {
line = line.replace(&format!("${{{key}}}"), value);
let pattern = format!("${key}");
let mut result = String::with_capacity(line.len());
let mut remaining = line.as_str();
while let Some(pos) = remaining.find(pattern.as_str()) {
result.push_str(&remaining[..pos]);
let after = &remaining[pos + pattern.len()..];
if after.starts_with(|c: char| c.is_alphanumeric() || c == '_') {
result.push('$');
remaining = &remaining[pos + 1..];
} else {
result.push_str(value);
remaining = after;
}
}
result.push_str(remaining);
result
}

fn find_primary_service(
docker_compose: &DockerComposeResources,
devcontainer: &DevContainerManifest,
Expand Down Expand Up @@ -5951,6 +5975,55 @@ FROM docker.io/hexpm/elixir:1.21-erlang-28.4.1-debian-trixie-20260316-slim AS de
assert_eq!(base_image, "test_image:latest");
}

#[gpui::test]
async fn test_expands_bare_dollar_args_in_dockerfile(cx: &mut TestAppContext) {
cx.executor().allow_parking();
env_logger::try_init().ok();
let given_devcontainer_contents = r#"
{
"name": "ruby-devcontainer",
"build": {
"dockerfile": "Dockerfile",
},
}
"#;

let (test_dependencies, mut devcontainer_manifest) =
init_default_devcontainer_manifest(cx, given_devcontainer_contents)
.await
.unwrap();

test_dependencies
.fs
.atomic_write(
PathBuf::from(TEST_PROJECT_PATH).join(".devcontainer/Dockerfile"),
// Mirrors real-world Dockerfiles that use bare $VAR instead of ${VAR}.
// $RUBY_VERSION2 must not be partially replaced when expanding $RUBY_VERSION.
r#"
ARG RUBY_VERSION=3.4.4
ARG RUBY_VERSION2=3.3.0
FROM ghcr.io/rails/devcontainer/images/ruby:$RUBY_VERSION
RUN echo $RUBY_VERSION2
"#
.trim()
.to_string(),
)
.await
.unwrap();

devcontainer_manifest.parse_nonremote_vars().unwrap();

let expanded = devcontainer_manifest
.expanded_dockerfile_content()
.await
.unwrap();

assert_eq!(
expanded,
"ARG RUBY_VERSION=3.4.4\nARG RUBY_VERSION2=3.3.0\nFROM ghcr.io/rails/devcontainer/images/ruby:3.4.4\nRUN echo 3.3.0"
);
}

#[cfg(not(target_os = "windows"))]
#[gpui::test]
async fn check_for_existing_container_errors_when_multiple_match(cx: &mut TestAppContext) {
Expand Down
Loading