use std::{ collections::HashMap, fmt::Debug, hash::{DefaultHasher, Hash, Hasher}, path::{Path, PathBuf}, sync::Arc,}; use regex::Regex; use fs::Fs;use http_client::HttpClient;use util::{ResultExt, command::Command, normalize_path}; use crate::{ DevContainerConfig, DevContainerContext, command_json::{CommandRunner, DefaultCommandRunner}, devcontainer_api::{DevContainerError, DevContainerUp}, devcontainer_json::{ ContainerBuild, DevContainer, DevContainerBuildType, FeatureOptions, ForwardPort, MountDefinition, deserialize_devcontainer_json, deserialize_devcontainer_json_from_value, deserialize_devcontainer_json_to_value, }, docker::{ Docker, DockerClient, DockerComposeConfig, DockerComposeService, DockerComposeServiceBuild, DockerComposeServicePort, DockerComposeVolume, DockerInspect, DockerPs, }, features::{DevContainerFeatureJson, FeatureManifest, parse_oci_feature_ref}, get_oci_token, oci::{TokenResponse, download_oci_tarball, get_oci_manifest}, safe_id_lower,}; enum ConfigStatus { Deserialized(DevContainer), VariableParsed(DevContainer),} #[derive(Debug, Clone, Eq, PartialEq, Default)]pub(crate) struct DockerComposeResources { files: Vec<PathBuf>, config: DockerComposeConfig,} struct DevContainerManifest { http_client: Arc<dyn HttpClient>, fs: Arc<dyn Fs>, docker_client: Arc<dyn DockerClient>, command_runner: Arc<dyn CommandRunner>, raw_config: String, config: ConfigStatus, local_environment: HashMap<String, String>, local_project_directory: PathBuf, config_directory: PathBuf, file_name: String, root_image: Option<DockerInspect>, features_build_info: Option<FeaturesBuildInfo>, features: Vec<FeatureManifest>,}const DEFAULT_REMOTE_PROJECT_DIR: &str = "/workspaces";impl DevContainerManifest { async fn new( context: &DevContainerContext, environment: HashMap<String, String>, docker_client: Arc<dyn DockerClient>, command_runner: Arc<dyn CommandRunner>, local_config: DevContainerConfig, local_project_path: &Path, ) -> Result<Self, DevContainerError> { let config_path = local_project_path.join(local_config.config_path.clone()); log::debug!("parsing devcontainer json found in {:?}", &config_path); let devcontainer_contents = context.fs.load(&config_path).await.map_err(|e| { log::error!("Unable to read devcontainer contents: {e}"); DevContainerError::DevContainerParseFailed })?; let devcontainer = deserialize_devcontainer_json(&devcontainer_contents)?; let devcontainer_directory = config_path.parent().ok_or_else(|| { log::error!("Dev container file should be in a directory"); DevContainerError::NotInValidProject })?; let file_name = config_path .file_name() .and_then(|f| f.to_str()) .ok_or_else(|| { log::error!("Dev container file has no file name, or is invalid unicode"); DevContainerError::DevContainerParseFailed })?; Ok(Self { fs: context.fs.clone(), http_client: context.http_client.clone(), docker_client, command_runner, raw_config: devcontainer_contents, config: ConfigStatus::Deserialized(devcontainer), local_project_directory: local_project_path.to_path_buf(), local_environment: environment, config_directory: devcontainer_directory.to_path_buf(), file_name: file_name.to_string(), root_image: None, features_build_info: None, features: Vec::new(), }) } fn devcontainer_id(&self) -> String { let mut labels = self.identifying_labels(); labels.sort_by_key(|(key, _)| *key); let mut hasher = DefaultHasher::new(); for (key, value) in &labels { key.hash(&mut hasher); value.hash(&mut hasher); } format!("{:016x}", hasher.finish()) } fn identifying_labels(&self) -> Vec<(&str, String)> { let labels = vec![ ( "devcontainer.local_folder", (self.local_project_directory.display()).to_string(), ), ( "devcontainer.config_file", (self.config_file().display()).to_string(), ), ]; labels } fn parse_nonremote_vars_for_content( &self, content: &str, ) -> Result<serde_json_lenient::Value, DevContainerError> { let mut value = deserialize_devcontainer_json_to_value(content)?; let mut to_visit = vec![&mut value]; while let Some(value) = to_visit.pop() { use serde_json_lenient::Value; match value { Value::String(string) => { *string = string .replace("${devcontainerId}", &self.devcontainer_id()) .replace( "${containerWorkspaceFolderBasename}", &self.remote_workspace_base_name().unwrap_or_default(), ) .replace( "${localWorkspaceFolderBasename}", &self.local_workspace_base_name()?, ) .replace( "${containerWorkspaceFolder}", &self .remote_workspace_folder() .map(|path| path.display().to_string()) .unwrap_or_default() .replace('\\', "/"), ) .replace( "${localWorkspaceFolder}", &self.local_workspace_folder().replace('\\', "/"), ); *string = Self::replace_environment_variables( string, "localEnv", &self.local_environment, ); } Value::Array(array) => to_visit.extend(array.iter_mut()), Value::Object(object) => to_visit.extend(object.values_mut()), Value::Null | Value::Bool(_) | Value::Number(_) => {} } } Ok(value) } fn parse_nonremote_vars(&mut self) -> Result<(), DevContainerError> { let replaced_content = self.parse_nonremote_vars_for_content(&self.raw_config)?; let parsed_config = deserialize_devcontainer_json_from_value(replaced_content)?; self.config = ConfigStatus::VariableParsed(parsed_config); Ok(()) } fn runtime_remote_env( &self, container_env: &HashMap<String, String>, ) -> Result<HashMap<String, String>, DevContainerError> { let mut merged_remote_env = container_env.clone(); // HOME is user-specific, and we will often not run as the image user merged_remote_env.remove("HOME"); if let Some(mut remote_env) = self.dev_container().remote_env.clone() { remote_env.values_mut().for_each(|value| { *value = Self::replace_environment_variables(value, "containerEnv", &container_env) }); for (k, v) in remote_env { merged_remote_env.insert(k, v); } } Ok(merged_remote_env) } fn replace_environment_variables( mut orig: &str, environment_source: &str, environment: &HashMap<String, String>, ) -> String { let mut replaced = String::with_capacity(orig.len()); let prefix = format!("${{{environment_source}:"); while let Some(start) = orig.find(&prefix) { let var_name_start = start + prefix.len(); let Some(end) = orig[var_name_start..].find('}') else { // No closing `}` => malformed variable reference => paste as is. break; }; let end = var_name_start + end; let (var_name_end, default_start) = if let Some(var_name_end) = orig[var_name_start..end].find(':') { let var_name_end = var_name_start + var_name_end; (var_name_end, var_name_end + 1) } else { (end, end) }; let var_name = &orig[var_name_start..var_name_end]; if var_name.is_empty() { // Empty variable name => paste as is. replaced.push_str(&orig[..end + 1]); orig = &orig[end + 1..]; continue; } let default = &orig[default_start..end]; replaced.push_str(&orig[..start]); replaced.push_str( environment .get(var_name) .map(|value| value.as_str()) .unwrap_or(default), ); orig = &orig[end + 1..]; } replaced.push_str(orig); replaced } fn config_file(&self) -> PathBuf { self.config_directory.join(&self.file_name) } fn dev_container(&self) -> &DevContainer { match &self.config { ConfigStatus::Deserialized(dev_container) => dev_container, ConfigStatus::VariableParsed(dev_container) => dev_container, } } async fn dockerfile_location(&self) -> Option<PathBuf> { let dev_container = self.dev_container(); match dev_container.build_type() { DevContainerBuildType::Image(_) => None, DevContainerBuildType::Dockerfile(build) => { Some(self.config_directory.join(&build.dockerfile)) } DevContainerBuildType::DockerCompose => { let Ok(docker_compose_manifest) = self.docker_compose_manifest().await else { return None; }; let Ok((_, main_service)) = find_primary_service(&docker_compose_manifest, self) else { return None; }; main_service.build.and_then(|b| { let compose_file = docker_compose_manifest.files.first()?; resolve_compose_dockerfile( compose_file, b.context.as_deref(), b.dockerfile.as_deref()?, ) }) } DevContainerBuildType::None => None, } } fn generate_features_image_tag(&self, dockerfile_build_path: String) -> String { let mut hasher = DefaultHasher::new(); let prefix = match &self.dev_container().name { Some(name) => &safe_id_lower(name), None => "zed-dc", }; let prefix = prefix.get(..6).unwrap_or(prefix); let prefix = prefix.trim_matches(|c: char| !c.is_alphanumeric()); dockerfile_build_path.hash(&mut hasher); let hash = hasher.finish(); format!("{}-{:x}-features", prefix, hash) } /// Gets the base image from the devcontainer with the following precedence: /// - The devcontainer image if an image is specified /// - The image sourced in the Dockerfile if a Dockerfile is specified /// - The image sourced in the docker-compose main service, if one is specified /// - The image sourced in the docker-compose main service dockerfile, if one is specified /// If no such image is available, return an error async fn get_base_image_from_config(&self) -> Result<String, DevContainerError> { match self.dev_container().build_type() { DevContainerBuildType::Image(image) => { return Ok(image); } DevContainerBuildType::Dockerfile(build) => { let dockerfile_contents = self.expanded_dockerfile_content().await?; return image_from_dockerfile(dockerfile_contents, &build.target).ok_or_else( || { log::error!("Unable to find base image in Dockerfile"); DevContainerError::DevContainerParseFailed }, ); } DevContainerBuildType::DockerCompose => { let docker_compose_manifest = self.docker_compose_manifest().await?; let (_, main_service) = find_primary_service(&docker_compose_manifest, &self)?; if let Some(_) = main_service .build .as_ref() .and_then(|b| b.dockerfile.as_ref()) { let dockerfile_contents = self.expanded_dockerfile_content().await?; return image_from_dockerfile( dockerfile_contents, &main_service.build.as_ref().and_then(|b| b.target.clone()), ) .ok_or_else(|| { log::error!("Unable to find base image in Dockerfile"); DevContainerError::DevContainerParseFailed }); } if let Some(image) = &main_service.image { return Ok(image.to_string()); } log::error!("No valid base image found in docker-compose configuration"); return Err(DevContainerError::DevContainerParseFailed); } DevContainerBuildType::None => { log::error!("Not a valid devcontainer config for build"); return Err(DevContainerError::NotInValidProject); } } } async fn download_feature_and_dockerfile_resources(&mut self) -> Result<(), DevContainerError> { let dev_container = match &self.config { ConfigStatus::Deserialized(_) => { log::error!( "Dev container has not yet been parsed for variable expansion. Cannot yet download resources" ); return Err(DevContainerError::DevContainerParseFailed); } ConfigStatus::VariableParsed(dev_container) => dev_container, }; let root_image_tag = self.get_base_image_from_config().await?; let root_image = self.docker_client.inspect(&root_image_tag).await?; let temp_base = std::env::temp_dir().join("devcontainer-zed"); let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_millis()) .unwrap_or(0); let features_content_dir = temp_base.join(format!("container-features-{}", timestamp)); let empty_context_dir = temp_base.join("empty-folder"); self.fs .create_dir(&features_content_dir) .await .map_err(|e| { log::error!("Failed to create features content dir: {e}"); DevContainerError::FilesystemError })?; self.fs.create_dir(&empty_context_dir).await.map_err(|e| { log::error!("Failed to create empty context dir: {e}"); DevContainerError::FilesystemError })?; let dockerfile_path = features_content_dir.join("Dockerfile.extended"); let image_tag = self.generate_features_image_tag(dockerfile_path.clone().display().to_string()); let build_info = FeaturesBuildInfo { dockerfile_path, features_content_dir, empty_context_dir, build_image: dev_container.image.clone(), image_tag, }; let features = match &dev_container.features { Some(features) => features, None => &HashMap::new(), }; let container_user = get_container_user_from_config(&root_image, self)?; let remote_user = get_remote_user_from_config(&root_image, self)?; let builtin_env_content = format!( "_CONTAINER_USER={}\n_REMOTE_USER={}\n", container_user, remote_user ); let builtin_env_path = build_info .features_content_dir .join("devcontainer-features.builtin.env"); self.fs .write(&builtin_env_path, &builtin_env_content.as_bytes()) .await .map_err(|e| { log::error!("Failed to write builtin env file: {e}"); DevContainerError::FilesystemError })?; let ordered_features = resolve_feature_order(features, &dev_container.override_feature_install_order); for (index, (feature_ref, options)) in ordered_features.iter().enumerate() { if matches!(options, FeatureOptions::Bool(false)) { log::debug!( "Feature '{}' is disabled (set to false), skipping", feature_ref ); continue; } let feature_id = extract_feature_id(feature_ref); let consecutive_id = format!("{}_{}", feature_id, index); let feature_dir = build_info.features_content_dir.join(&consecutive_id); self.fs.create_dir(&feature_dir).await.map_err(|e| { log::error!( "Failed to create feature directory for {}: {e}", feature_ref ); DevContainerError::FilesystemError })?; let oci_ref = parse_oci_feature_ref(feature_ref).ok_or_else(|| { log::error!( "Feature '{}' is not a supported OCI feature reference", feature_ref ); DevContainerError::DevContainerParseFailed })?; let TokenResponse { token } = get_oci_token(&oci_ref.registry, &oci_ref.path, &self.http_client) .await .map_err(|e| { log::error!("Failed to get OCI token for feature '{}': {e}", feature_ref); DevContainerError::ResourceFetchFailed })?; let manifest = get_oci_manifest( &oci_ref.registry, &oci_ref.path, &token, &self.http_client, &oci_ref.version, None, ) .await .map_err(|e| { log::error!( "Failed to fetch OCI manifest for feature '{}': {e}", feature_ref ); DevContainerError::ResourceFetchFailed })?; let digest = &manifest .layers .first() .ok_or_else(|| { log::error!( "OCI manifest for feature '{}' contains no layers", feature_ref ); DevContainerError::ResourceFetchFailed })?Lines 1–500 of 8093. Load more
use std::{ collections::HashMap, fmt::Debug, hash::{DefaultHasher, Hash, Hasher}, path::{Path, PathBuf}, sync::Arc, }; use regex::Regex; use fs::Fs; use http_client::HttpClient; use util::{ResultExt, command::Command, normalize_path}; use crate::{ DevContainerConfig, DevContainerContext, command_json::{CommandRunner, DefaultCommandRunner}, devcontainer_api::{DevContainerError, DevContainerUp}, devcontainer_json::{ ContainerBuild, DevContainer, DevContainerBuildType, FeatureOptions, ForwardPort, MountDefinition, deserialize_devcontainer_json, deserialize_devcontainer_json_from_value, deserialize_devcontainer_json_to_value, }, docker::{ Docker, DockerClient, DockerComposeConfig, DockerComposeService, DockerComposeServiceBuild, DockerComposeServicePort, DockerComposeVolume, DockerInspect, DockerPs, }, features::{DevContainerFeatureJson, FeatureManifest, parse_oci_feature_ref}, get_oci_token, oci::{TokenResponse, download_oci_tarball, get_oci_manifest}, safe_id_lower, }; enum ConfigStatus { Deserialized(DevContainer), VariableParsed(DevContainer), } #[derive(Debug, Clone, Eq, PartialEq, Default)] pub(crate) struct DockerComposeResources { files: Vec<PathBuf>, config: DockerComposeConfig, } struct DevContainerManifest { http_client: Arc<dyn HttpClient>, fs: Arc<dyn Fs>, docker_client: Arc<dyn DockerClient>, command_runner: Arc<dyn CommandRunner>, raw_config: String, config: ConfigStatus, local_environment: HashMap<String, String>, local_project_directory: PathBuf, config_directory: PathBuf, file_name: String, root_image: Option<DockerInspect>, features_build_info: Option<FeaturesBuildInfo>, features: Vec<FeatureManifest>, } const DEFAULT_REMOTE_PROJECT_DIR: &str = "/workspaces"; impl DevContainerManifest { async fn new( context: &DevContainerContext, environment: HashMap<String, String>, docker_client: Arc<dyn DockerClient>, command_runner: Arc<dyn CommandRunner>, local_config: DevContainerConfig, local_project_path: &Path, ) -> Result<Self, DevContainerError> { let config_path = local_project_path.join(local_config.config_path.clone()); log::debug!("parsing devcontainer json found in {:?}", &config_path); let devcontainer_contents = context.fs.load(&config_path).await.map_err(|e| { log::error!("Unable to read devcontainer contents: {e}"); DevContainerError::DevContainerParseFailed })?; let devcontainer = deserialize_devcontainer_json(&devcontainer_contents)?; let devcontainer_directory = config_path.parent().ok_or_else(|| { log::error!("Dev container file should be in a directory"); DevContainerError::NotInValidProject })?; let file_name = config_path .file_name() .and_then(|f| f.to_str()) .ok_or_else(|| { log::error!("Dev container file has no file name, or is invalid unicode"); DevContainerError::DevContainerParseFailed })?; Ok(Self { fs: context.fs.clone(), http_client: context.http_client.clone(), docker_client, command_runner, raw_config: devcontainer_contents, config: ConfigStatus::Deserialized(devcontainer), local_project_directory: local_project_path.to_path_buf(), local_environment: environment, config_directory: devcontainer_directory.to_path_buf(), file_name: file_name.to_string(), root_image: None, features_build_info: None, features: Vec::new(), }) } fn devcontainer_id(&self) -> String { let mut labels = self.identifying_labels(); labels.sort_by_key(|(key, _)| *key); let mut hasher = DefaultHasher::new(); for (key, value) in &labels { key.hash(&mut hasher); value.hash(&mut hasher); } format!("{:016x}", hasher.finish()) } fn identifying_labels(&self) -> Vec<(&str, String)> { let labels = vec![ ( "devcontainer.local_folder", (self.local_project_directory.display()).to_string(), ), ( "devcontainer.config_file", (self.config_file().display()).to_string(), ), ]; labels } fn parse_nonremote_vars_for_content( &self, content: &str, ) -> Result<serde_json_lenient::Value, DevContainerError> { let mut value = deserialize_devcontainer_json_to_value(content)?; let mut to_visit = vec![&mut value]; while let Some(value) = to_visit.pop() { use serde_json_lenient::Value; match value { Value::String(string) => { *string = string .replace("${devcontainerId}", &self.devcontainer_id()) .replace( "${containerWorkspaceFolderBasename}", &self.remote_workspace_base_name().unwrap_or_default(), ) .replace( "${localWorkspaceFolderBasename}", &self.local_workspace_base_name()?, ) .replace( "${containerWorkspaceFolder}", &self .remote_workspace_folder() .map(|path| path.display().to_string()) .unwrap_or_default() .replace('\\', "/"), ) .replace( "${localWorkspaceFolder}", &self.local_workspace_folder().replace('\\', "/"), ); *string = Self::replace_environment_variables( string, "localEnv", &self.local_environment, ); } Value::Array(array) => to_visit.extend(array.iter_mut()), Value::Object(object) => to_visit.extend(object.values_mut()), Value::Null | Value::Bool(_) | Value::Number(_) => {} } } Ok(value) } fn parse_nonremote_vars(&mut self) -> Result<(), DevContainerError> { let replaced_content = self.parse_nonremote_vars_for_content(&self.raw_config)?; let parsed_config = deserialize_devcontainer_json_from_value(replaced_content)?; self.config = ConfigStatus::VariableParsed(parsed_config); Ok(()) } fn runtime_remote_env( &self, container_env: &HashMap<String, String>, ) -> Result<HashMap<String, String>, DevContainerError> { let mut merged_remote_env = container_env.clone(); // HOME is user-specific, and we will often not run as the image user merged_remote_env.remove("HOME"); if let Some(mut remote_env) = self.dev_container().remote_env.clone() { remote_env.values_mut().for_each(|value| { *value = Self::replace_environment_variables(value, "containerEnv", &container_env) }); for (k, v) in remote_env { merged_remote_env.insert(k, v); } } Ok(merged_remote_env) } fn replace_environment_variables( mut orig: &str, environment_source: &str, environment: &HashMap<String, String>, ) -> String { let mut replaced = String::with_capacity(orig.len()); let prefix = format!("${{{environment_source}:"); while let Some(start) = orig.find(&prefix) { let var_name_start = start + prefix.len(); let Some(end) = orig[var_name_start..].find('}') else { // No closing `}` => malformed variable reference => paste as is. break; }; let end = var_name_start + end; let (var_name_end, default_start) = if let Some(var_name_end) = orig[var_name_start..end].find(':') { let var_name_end = var_name_start + var_name_end; (var_name_end, var_name_end + 1) } else { (end, end) }; let var_name = &orig[var_name_start..var_name_end]; if var_name.is_empty() { // Empty variable name => paste as is. replaced.push_str(&orig[..end + 1]); orig = &orig[end + 1..]; continue; } let default = &orig[default_start..end]; replaced.push_str(&orig[..start]); replaced.push_str( environment .get(var_name) .map(|value| value.as_str()) .unwrap_or(default), ); orig = &orig[end + 1..]; } replaced.push_str(orig); replaced } fn config_file(&self) -> PathBuf { self.config_directory.join(&self.file_name) } fn dev_container(&self) -> &DevContainer { match &self.config { ConfigStatus::Deserialized(dev_container) => dev_container, ConfigStatus::VariableParsed(dev_container) => dev_container, } } async fn dockerfile_location(&self) -> Option<PathBuf> { let dev_container = self.dev_container(); match dev_container.build_type() { DevContainerBuildType::Image(_) => None, DevContainerBuildType::Dockerfile(build) => { Some(self.config_directory.join(&build.dockerfile)) } DevContainerBuildType::DockerCompose => { let Ok(docker_compose_manifest) = self.docker_compose_manifest().await else { return None; }; let Ok((_, main_service)) = find_primary_service(&docker_compose_manifest, self) else { return None; }; main_service.build.and_then(|b| { let compose_file = docker_compose_manifest.files.first()?; resolve_compose_dockerfile( compose_file, b.context.as_deref(), b.dockerfile.as_deref()?, ) }) } DevContainerBuildType::None => None, } } fn generate_features_image_tag(&self, dockerfile_build_path: String) -> String { let mut hasher = DefaultHasher::new(); let prefix = match &self.dev_container().name { Some(name) => &safe_id_lower(name), None => "zed-dc", }; let prefix = prefix.get(..6).unwrap_or(prefix); let prefix = prefix.trim_matches(|c: char| !c.is_alphanumeric()); dockerfile_build_path.hash(&mut hasher); let hash = hasher.finish(); format!("{}-{:x}-features", prefix, hash) } /// Gets the base image from the devcontainer with the following precedence: /// - The devcontainer image if an image is specified /// - The image sourced in the Dockerfile if a Dockerfile is specified /// - The image sourced in the docker-compose main service, if one is specified /// - The image sourced in the docker-compose main service dockerfile, if one is specified /// If no such image is available, return an error async fn get_base_image_from_config(&self) -> Result<String, DevContainerError> { match self.dev_container().build_type() { DevContainerBuildType::Image(image) => { return Ok(image); } DevContainerBuildType::Dockerfile(build) => { let dockerfile_contents = self.expanded_dockerfile_content().await?; return image_from_dockerfile(dockerfile_contents, &build.target).ok_or_else( || { log::error!("Unable to find base image in Dockerfile"); DevContainerError::DevContainerParseFailed }, ); } DevContainerBuildType::DockerCompose => { let docker_compose_manifest = self.docker_compose_manifest().await?; let (_, main_service) = find_primary_service(&docker_compose_manifest, &self)?; if let Some(_) = main_service .build .as_ref() .and_then(|b| b.dockerfile.as_ref()) { let dockerfile_contents = self.expanded_dockerfile_content().await?; return image_from_dockerfile( dockerfile_contents, &main_service.build.as_ref().and_then(|b| b.target.clone()), ) .ok_or_else(|| { log::error!("Unable to find base image in Dockerfile"); DevContainerError::DevContainerParseFailed }); } if let Some(image) = &main_service.image { return Ok(image.to_string()); } log::error!("No valid base image found in docker-compose configuration"); return Err(DevContainerError::DevContainerParseFailed); } DevContainerBuildType::None => { log::error!("Not a valid devcontainer config for build"); return Err(DevContainerError::NotInValidProject); } } } async fn download_feature_and_dockerfile_resources(&mut self) -> Result<(), DevContainerError> { let dev_container = match &self.config { ConfigStatus::Deserialized(_) => { log::error!( "Dev container has not yet been parsed for variable expansion. Cannot yet download resources" ); return Err(DevContainerError::DevContainerParseFailed); } ConfigStatus::VariableParsed(dev_container) => dev_container, }; let root_image_tag = self.get_base_image_from_config().await?; let root_image = self.docker_client.inspect(&root_image_tag).await?; let temp_base = std::env::temp_dir().join("devcontainer-zed"); let timestamp = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map(|d| d.as_millis()) .unwrap_or(0); let features_content_dir = temp_base.join(format!("container-features-{}", timestamp)); let empty_context_dir = temp_base.join("empty-folder"); self.fs .create_dir(&features_content_dir) .await .map_err(|e| { log::error!("Failed to create features content dir: {e}"); DevContainerError::FilesystemError })?; self.fs.create_dir(&empty_context_dir).await.map_err(|e| { log::error!("Failed to create empty context dir: {e}"); DevContainerError::FilesystemError })?; let dockerfile_path = features_content_dir.join("Dockerfile.extended"); let image_tag = self.generate_features_image_tag(dockerfile_path.clone().display().to_string()); let build_info = FeaturesBuildInfo { dockerfile_path, features_content_dir, empty_context_dir, build_image: dev_container.image.clone(), image_tag, }; let features = match &dev_container.features { Some(features) => features, None => &HashMap::new(), }; let container_user = get_container_user_from_config(&root_image, self)?; let remote_user = get_remote_user_from_config(&root_image, self)?; let builtin_env_content = format!( "_CONTAINER_USER={}\n_REMOTE_USER={}\n", container_user, remote_user ); let builtin_env_path = build_info .features_content_dir .join("devcontainer-features.builtin.env"); self.fs .write(&builtin_env_path, &builtin_env_content.as_bytes()) .await .map_err(|e| { log::error!("Failed to write builtin env file: {e}"); DevContainerError::FilesystemError })?; let ordered_features = resolve_feature_order(features, &dev_container.override_feature_install_order); for (index, (feature_ref, options)) in ordered_features.iter().enumerate() { if matches!(options, FeatureOptions::Bool(false)) { log::debug!( "Feature '{}' is disabled (set to false), skipping", feature_ref ); continue; } let feature_id = extract_feature_id(feature_ref); let consecutive_id = format!("{}_{}", feature_id, index); let feature_dir = build_info.features_content_dir.join(&consecutive_id); self.fs.create_dir(&feature_dir).await.map_err(|e| { log::error!( "Failed to create feature directory for {}: {e}", feature_ref ); DevContainerError::FilesystemError })?; let oci_ref = parse_oci_feature_ref(feature_ref).ok_or_else(|| { log::error!( "Feature '{}' is not a supported OCI feature reference", feature_ref ); DevContainerError::DevContainerParseFailed })?; let TokenResponse { token } = get_oci_token(&oci_ref.registry, &oci_ref.path, &self.http_client) .await .map_err(|e| { log::error!("Failed to get OCI token for feature '{}': {e}", feature_ref); DevContainerError::ResourceFetchFailed })?; let manifest = get_oci_manifest( &oci_ref.registry, &oci_ref.path, &token, &self.http_client, &oci_ref.version, None, ) .await .map_err(|e| { log::error!( "Failed to fetch OCI manifest for feature '{}': {e}", feature_ref ); DevContainerError::ResourceFetchFailed })?; let digest = &manifest .layers .first() .ok_or_else(|| { log::error!( "OCI manifest for feature '{}' contains no layers", feature_ref ); DevContainerError::ResourceFetchFailed })?