diff --git a/crates/dev_container/src/devcontainer_manifest.rs b/crates/dev_container/src/devcontainer_manifest.rs index fe5a5e1837f47128ab70bf20ffc53571d892bb01..952b48bd9b259500a3d67185c92dad4ad9e28778 100644 --- a/crates/dev_container/src/devcontainer_manifest.rs +++ b/crates/dev_container/src/devcontainer_manifest.rs @@ -2113,12 +2113,24 @@ RUN sed -i -E 's/((^|\s)PATH=)([^\$]*)$/\1\${PATH:-\3}/g' /etc/profile || true return Err(DevContainerError::DevContainerParseFailed); }; - let devcontainer_args = self - .dev_container() - .build - .as_ref() - .and_then(|b| b.args.clone()) - .unwrap_or_default(); + // For docker-compose configs the build args live on the primary + // compose service rather than on dev_container.build. + let devcontainer_args = match self.dev_container().build_type() { + DevContainerBuildType::DockerCompose => { + let compose = self.docker_compose_manifest().await?; + find_primary_service(&compose, self)? + .1 + .build + .and_then(|b| b.args) + .unwrap_or_default() + } + _ => self + .dev_container() + .build + .as_ref() + .and_then(|b| b.args.clone()) + .unwrap_or_default(), + }; let contents = self.fs.load(&dockerfile_path).await.map_err(|e| { log::error!("Failed to load Dockerfile: {e}"); DevContainerError::FilesystemError @@ -5066,6 +5078,46 @@ FROM docker.io/hexpm/elixir:1.21-erlang-28.4.1-debian-trixie-20260316-slim AS de ) } + #[gpui::test] + async fn test_expands_compose_service_args_in_dockerfile(cx: &mut TestAppContext) { + cx.executor().allow_parking(); + env_logger::try_init().ok(); + + let given_devcontainer_contents = r#" + { + "dockerComposeFile": "docker-compose-with-args.yml", + "service": "app", + } + "#; + + 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"), + "FROM ${BASE_IMAGE}\nUSER root\n".to_string(), + ) + .await + .unwrap(); + + devcontainer_manifest.parse_nonremote_vars().unwrap(); + + let expanded = devcontainer_manifest + .expanded_dockerfile_content() + .await + .unwrap(); + + assert_eq!(expanded, "FROM test_image:latest\nUSER root"); + + let base_image = + image_from_dockerfile(expanded, &None).expect("base image resolves from compose args"); + assert_eq!(base_image, "test_image:latest"); + } + #[test] fn test_aliases_dockerfile_with_pre_existing_aliases_for_build() {} @@ -5306,6 +5358,35 @@ FROM docker.io/hexpm/elixir:1.21-erlang-28.4.1-debian-trixie-20260316-slim AS de volumes: HashMap::new(), })); } + if config_files.len() == 1 + && config_files.get(0) + == Some( + &project_path + .join(".devcontainer") + .join("docker-compose-with-args.yml"), + ) + { + return Ok(Some(DockerComposeConfig { + name: None, + services: HashMap::from([( + "app".to_string(), + DockerComposeService { + build: Some(DockerComposeServiceBuild { + context: Some(".".to_string()), + dockerfile: Some("Dockerfile".to_string()), + args: Some(HashMap::from([( + "BASE_IMAGE".to_string(), + "test_image:latest".to_string(), + )])), + additional_contexts: None, + target: None, + }), + ..Default::default() + }, + )]), + ..Default::default() + })); + } if config_files.len() == 1 && config_files.get(0) == Some(