diff --git a/crates/dev_container/src/devcontainer_manifest.rs b/crates/dev_container/src/devcontainer_manifest.rs index afe02b4950..fb2e4f6cd6 100644 --- a/crates/dev_container/src/devcontainer_manifest.rs +++ b/crates/dev_container/src/devcontainer_manifest.rs @@ -1541,6 +1541,14 @@ RUN sed -i -E 's/((^|\s)PATH=)([^\$]*)$/\1\${{PATH:-\3}}/g' /etc/profile || true let updated_image_tag = features_build_info.image_tag.clone(); let mut command = Command::new(self.docker_client.docker_cli()); + // Without a usable BuildKit, force the classic builder: the build's + // `FROM $BASE_IMAGE` references the locally-built features image, which + // only resolves from the daemon's image store under the classic builder. + if !self.docker_client.supports_compose_buildkit() + && self.docker_client.docker_cli() != "podman" + { + command.env("DOCKER_BUILDKIT", "0"); + } command.args(["build"]); command.args(["-f", &dockerfile_path.display().to_string()]); command.args(["-t", &updated_image_tag]); @@ -1644,6 +1652,12 @@ RUN sed -i -E 's/((^|\s)PATH=)([^\$]*)$/\1\${PATH:-\3}/g' /etc/profile || true })?; let mut command = Command::new(self.docker_client.docker_cli()); + // This path runs only when BuildKit is unavailable, so force the classic + // builder: the feature content image is consumed by a later multi-stage + // `FROM`, which requires it to live in the daemon's image store. + if self.docker_client.docker_cli() != "podman" { + command.env("DOCKER_BUILDKIT", "0"); + } command.args([ "build", "-t", @@ -2375,9 +2389,9 @@ pub(crate) async fn read_devcontainer_configuration( environment: HashMap, ) -> Result { let docker = if context.use_podman { - Docker::new("podman").await + Docker::new("podman", context.use_buildkit).await } else { - Docker::new("docker").await + Docker::new("docker", context.use_buildkit).await }; let mut dev_container = DevContainerManifest::new( context, @@ -2399,9 +2413,9 @@ pub(crate) async fn spawn_dev_container( local_project_path: &Path, ) -> Result { let docker = if context.use_podman { - Docker::new("podman").await + Docker::new("podman", context.use_buildkit).await } else { - Docker::new("docker").await + Docker::new("docker", context.use_buildkit).await }; let mut devcontainer_manifest = DevContainerManifest::new( context, @@ -3062,6 +3076,7 @@ mod test { let context = DevContainerContext { project_directory: SanitizedPath::cast_arc(project_path), use_podman: false, + use_buildkit: None, fs: fs.clone(), http_client: http_client.clone(), environment: project_environment.downgrade(), diff --git a/crates/dev_container/src/docker.rs b/crates/dev_container/src/docker.rs index 6a583681ac..6e3e82dc76 100644 --- a/crates/dev_container/src/docker.rs +++ b/crates/dev_container/src/docker.rs @@ -192,9 +192,18 @@ impl DockerInspect { } impl Docker { - pub(crate) async fn new(docker_cli: &str) -> Self { + pub(crate) async fn new(docker_cli: &str, use_buildkit: Option) -> Self { let has_buildx = if docker_cli == "podman" { false + } else if let Some(use_buildkit) = use_buildkit { + // Honor the explicit `dev_container_use_buildkit` setting. Setting it + // to `false` forces the classic Docker builder for Docker-compatible + // engines that lack an integrated BuildKit (e.g. Apple Container via + // a Docker-API bridge), where BuildKit builds cannot resolve + // locally-built images. The classic builder builds the feature + // content as an image and references it with an ordinary + // multi-stage `FROM`. + use_buildkit } else { let output = Command::new(docker_cli) .args(["buildx", "version"]) @@ -204,7 +213,7 @@ impl Docker { }; if !has_buildx && docker_cli != "podman" { log::info!( - "docker buildx not found; dev container builds will use the scratch-image fallback" + "Using the classic Docker builder for dev container builds (BuildKit unavailable or disabled)" ); } Self { @@ -295,7 +304,15 @@ impl DockerClient for Docker { ) -> Result<(), DevContainerError> { let mut command = Command::new(&self.docker_cli); if !self.is_podman() { - command.env("DOCKER_BUILDKIT", "1"); + if self.has_buildx { + command.env("DOCKER_BUILDKIT", "1"); + } else { + // Without a usable BuildKit, build through the classic builder so + // multi-stage `FROM` of locally-built images (the feature content + // image) resolves from the daemon's image store. + command.env("DOCKER_BUILDKIT", "0"); + command.env("COMPOSE_DOCKER_CLI_BUILD", "0"); + } } command.args(&["compose", "--project-name", project_name]); for docker_compose_file in config_files { @@ -694,11 +711,33 @@ mod test { devcontainer_api::DevContainerError, devcontainer_json::MountDefinition, docker::{ - Docker, DockerComposeConfig, DockerComposeService, DockerComposeServicePort, - DockerComposeVolume, DockerInspect, DockerPs, parse_find_process_output, + Docker, DockerClient, DockerComposeConfig, DockerComposeService, + DockerComposeServicePort, DockerComposeVolume, DockerInspect, DockerPs, + parse_find_process_output, }, }; + #[test] + fn use_buildkit_setting_overrides_buildx_detection() { + // `Some(_)` short-circuits the `buildx version` probe, so these run + // without invoking docker. + let forced_off = futures::executor::block_on(Docker::new("docker", Some(false))); + assert!( + !forced_off.supports_compose_buildkit(), + "use_buildkit=false must force the classic builder" + ); + + let forced_on = futures::executor::block_on(Docker::new("docker", Some(true))); + assert!( + forced_on.supports_compose_buildkit(), + "use_buildkit=true must enable BuildKit" + ); + + // podman never supports the BuildKit/buildx path, regardless of the setting. + let podman = futures::executor::block_on(Docker::new("podman", Some(true))); + assert!(!podman.supports_compose_buildkit()); + } + #[test] fn should_parse_simple_env_var() { let config = super::DockerInspectConfig { diff --git a/crates/dev_container/src/lib.rs b/crates/dev_container/src/lib.rs index 601394c777..833caa830f 100644 --- a/crates/dev_container/src/lib.rs +++ b/crates/dev_container/src/lib.rs @@ -98,6 +98,7 @@ fn get_safe_id(input: &str) -> String { pub struct DevContainerContext { pub project_directory: Arc, pub use_podman: bool, + pub use_buildkit: Option, pub fs: Arc, pub http_client: Arc, pub environment: WeakEntity, @@ -106,13 +107,16 @@ pub struct DevContainerContext { impl DevContainerContext { pub fn from_workspace(workspace: &Workspace, cx: &App) -> Option { let project_directory = workspace.project().read(cx).active_project_directory(cx)?; - let use_podman = DevContainerSettings::get_global(cx).use_podman; + let settings = DevContainerSettings::get_global(cx); + let use_podman = settings.use_podman; + let use_buildkit = settings.use_buildkit; let http_client = cx.http_client().clone(); let fs = workspace.app_state().fs.clone(); let environment = workspace.project().read(cx).environment().downgrade(); Some(Self { project_directory, use_podman, + use_buildkit, fs, http_client, environment, @@ -134,6 +138,7 @@ impl DevContainerContext { #[derive(RegisterSetting)] struct DevContainerSettings { use_podman: bool, + use_buildkit: Option, } pub fn use_podman(cx: &App) -> bool { @@ -144,6 +149,7 @@ impl Settings for DevContainerSettings { fn from_settings(content: &settings::SettingsContent) -> Self { Self { use_podman: content.remote.use_podman.unwrap_or(false), + use_buildkit: content.remote.dev_container_use_buildkit, } } } diff --git a/crates/settings_content/src/settings_content.rs b/crates/settings_content/src/settings_content.rs index 9ac8b4b14f..0ae76ccf56 100644 --- a/crates/settings_content/src/settings_content.rs +++ b/crates/settings_content/src/settings_content.rs @@ -1135,6 +1135,16 @@ pub struct RemoteSettingsContent { pub dev_container_connections: Option>, pub read_ssh_config: Option, pub use_podman: Option, + /// Whether to build dev container images with BuildKit. + /// + /// When unset, Zed auto-detects BuildKit by probing for the `buildx` CLI + /// plugin. Set to `false` to force the classic Docker builder, which is + /// required for Docker-compatible engines that lack an integrated BuildKit + /// (e.g. Apple Container via a Docker-API bridge), where BuildKit builds + /// cannot resolve locally-built images. + /// + /// Default: null (auto-detect) + pub dev_container_use_buildkit: Option, } #[with_fallible_options] diff --git a/docs/src/dev-containers.md b/docs/src/dev-containers.md index 3b0874812b..e3765ab522 100644 --- a/docs/src/dev-containers.md +++ b/docs/src/dev-containers.md @@ -14,6 +14,8 @@ If your repository includes a `.devcontainer/devcontainer.json` file, Zed can op - Docker or Podman must be installed and available in your `PATH`. If you use `podman`, you must set the `use_podman` setting in your Zed settings.json to true. - Your project must contain a `.devcontainer/devcontainer.json` directory/file. +By default Zed builds dev container images with BuildKit when the `docker buildx` plugin is available. If your Docker-compatible engine lacks an integrated BuildKit (for example, Apple Container accessed through a Docker-API bridge), set `"dev_container_use_buildkit": false` in your settings.json to use the classic Docker builder instead. + ## Using Dev Containers in Zed ### Automatic prompt