use std::{collections::HashMap, fmt::Display, path::Path, sync::Arc}; use crate::{command_json::CommandRunner, devcontainer_api::DevContainerError};use serde::{Deserialize, Deserializer, Serialize};use serde_json_lenient::Value;use util::command::Command; #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)]#[serde(untagged)]pub(crate) enum ForwardPort { Number(u16), String(String),} #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]#[serde(rename_all = "camelCase")]pub(crate) enum PortAttributeProtocol { Https, Http,} #[derive(Clone, Debug, Default, Deserialize, Serialize, Eq, PartialEq)]#[serde(rename_all = "camelCase")]pub(crate) enum OnAutoForward { #[default] Notify, OpenBrowser, OpenBrowserOnce, OpenPreview, Silent, Ignore,} #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]#[serde(rename_all = "camelCase")]pub(crate) struct PortAttributes { #[serde(default)] label: Option<String>, #[serde(default)] on_auto_forward: OnAutoForward, #[serde(default)] elevate_if_needed: bool, #[serde(default)] require_local_port: bool, #[serde(default)] protocol: Option<PortAttributeProtocol>,} #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]#[serde(rename_all = "camelCase")]pub(crate) enum UserEnvProbe { None, InteractiveShell, LoginShell, LoginInteractiveShell,} #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]#[serde(rename_all = "camelCase")]pub(crate) enum ShutdownAction { None, StopContainer, StopCompose,} #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]#[serde(rename_all = "camelCase")]pub(crate) struct MountDefinition { #[serde(default)] pub(crate) source: Option<String>, pub(crate) target: String, #[serde(rename = "type")] pub(crate) mount_type: Option<String>,} impl Display for MountDefinition { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mount_type = self.mount_type.clone().unwrap_or_else(|| { if let Some(source) = &self.source { if source.starts_with('/') || source.starts_with("\\\\") || source.get(1..3) == Some(":\\") || source.get(1..3) == Some(":/") { return "bind".to_string(); } } "volume".to_string() }); write!(f, "type={}", mount_type)?; if let Some(source) = &self.source { write!(f, ",source={}", source)?; } write!(f, ",target={},consistency=cached", self.target) }} /// Represents the value associated with a feature ID in the `features` map of devcontainer.json.////// Per the spec, the value can be:/// - A boolean (`true` to enable with defaults)/// - A string (shorthand for `{"version": "<value>"}`)/// - An object mapping option names to string or boolean values////// See: https://containers.dev/implementors/features/#devcontainerjson-properties#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)]#[serde(untagged)]pub(crate) enum FeatureOptions { Bool(bool), String(String), Options(HashMap<String, FeatureOptionValue>),} #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)]#[serde(untagged)]pub(crate) enum FeatureOptionValue { Bool(bool), String(String),}impl std::fmt::Display for FeatureOptionValue { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { FeatureOptionValue::Bool(b) => write!(f, "{}", b), FeatureOptionValue::String(s) => write!(f, "{}", s), } }} #[derive(Clone, Debug, Serialize, Eq, PartialEq, Default)]pub(crate) struct ZedCustomizationsWrapper { pub(crate) zed: ZedCustomization,} #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq, Default)]pub(crate) struct ZedCustomization { #[serde(default)] pub(crate) extensions: Vec<String>,} #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)]#[serde(rename_all = "camelCase")]pub(crate) struct ContainerBuild { pub(crate) dockerfile: String, pub(crate) context: Option<String>, pub(crate) args: Option<HashMap<String, String>>, pub(crate) options: Option<Vec<String>>, pub(crate) target: Option<String>, #[serde(default, deserialize_with = "deserialize_string_or_array")] pub(crate) cache_from: Option<Vec<String>>,} #[derive(Clone, Debug, Serialize, Eq, PartialEq)]struct LifecycleScriptInternal { command: Option<String>, args: Vec<String>,} impl LifecycleScriptInternal { fn from_args(args: Vec<String>) -> Self { let command = args.get(0).map(|a| a.to_string()); let remaining = args.iter().skip(1).map(|a| a.to_string()).collect(); Self { command, args: remaining, } }} #[derive(Clone, Debug, Serialize, Eq, PartialEq)]pub struct LifecycleScript { scripts: HashMap<String, LifecycleScriptInternal>,} #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]#[serde(rename_all = "camelCase")]pub(crate) struct HostRequirements { cpus: Option<u16>, memory: Option<String>, storage: Option<String>,} #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]#[serde(rename_all = "camelCase")]pub(crate) enum LifecycleCommand { InitializeCommand, OnCreateCommand, UpdateContentCommand, PostCreateCommand, PostStartCommand,} #[derive(Debug, PartialEq, Eq)]pub(crate) enum DevContainerBuildType { Image(String), Dockerfile(ContainerBuild), DockerCompose, None,}#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq, Default)]#[serde(rename_all = "camelCase")]pub(crate) struct DevContainer { pub(crate) image: Option<String>, pub(crate) name: Option<String>, pub(crate) remote_user: Option<String>, pub(crate) forward_ports: Option<Vec<ForwardPort>>, pub(crate) ports_attributes: Option<HashMap<String, PortAttributes>>, pub(crate) other_ports_attributes: Option<PortAttributes>, pub(crate) container_env: Option<HashMap<String, String>>, pub(crate) remote_env: Option<HashMap<String, String>>, pub(crate) container_user: Option<String>, #[serde(rename = "updateRemoteUserUID")] pub(crate) update_remote_user_uid: Option<bool>, user_env_probe: Option<UserEnvProbe>, pub(crate) override_command: Option<bool>, shutdown_action: Option<ShutdownAction>, pub(crate) init: Option<bool>, pub(crate) privileged: Option<bool>, pub(crate) cap_add: Option<Vec<String>>, pub(crate) security_opt: Option<Vec<String>>, #[serde(default, deserialize_with = "deserialize_mount_definitions")] pub(crate) mounts: Option<Vec<MountDefinition>>, pub(crate) features: Option<HashMap<String, FeatureOptions>>, pub(crate) override_feature_install_order: Option<Vec<String>>, pub(crate) customizations: Option<ZedCustomizationsWrapper>, pub(crate) build: Option<ContainerBuild>, #[serde(default, deserialize_with = "deserialize_app_port")] pub(crate) app_port: Vec<String>, #[serde(default, deserialize_with = "deserialize_mount_definition")] pub(crate) workspace_mount: Option<MountDefinition>, pub(crate) workspace_folder: Option<String>, pub(crate) run_args: Option<Vec<String>>, #[serde(default, deserialize_with = "deserialize_string_or_array")] pub(crate) docker_compose_file: Option<Vec<String>>, pub(crate) service: Option<String>, pub(crate) run_services: Option<Vec<String>>, pub(crate) initialize_command: Option<LifecycleScript>, pub(crate) on_create_command: Option<LifecycleScript>, pub(crate) update_content_command: Option<LifecycleScript>, pub(crate) post_create_command: Option<LifecycleScript>, pub(crate) post_start_command: Option<LifecycleScript>, pub(crate) post_attach_command: Option<LifecycleScript>, wait_for: Option<LifecycleCommand>, host_requirements: Option<HostRequirements>,} pub(crate) fn deserialize_devcontainer_json_to_value( json: &str,) -> Result<serde_json_lenient::Value, DevContainerError> { serde_json_lenient::from_str(json).map_err(|e| { log::error!("Unable to deserialize json values: {e}"); DevContainerError::DevContainerParseFailed })} pub(crate) fn deserialize_devcontainer_json_from_value( json: serde_json_lenient::Value,) -> Result<DevContainer, DevContainerError> { serde_json_lenient::from_value(json).map_err(|e| { log::error!("Unable to deserialize devcontainer from json values: {e}"); DevContainerError::DevContainerParseFailed })} pub(crate) fn deserialize_devcontainer_json(json: &str) -> Result<DevContainer, DevContainerError> { deserialize_devcontainer_json_to_value(json).and_then(deserialize_devcontainer_json_from_value)} impl DevContainer { pub(crate) fn build_type(&self) -> DevContainerBuildType { if let Some(image) = &self.image { DevContainerBuildType::Image(image.clone()) } else if self.docker_compose_file.is_some() { DevContainerBuildType::DockerCompose } else if let Some(build) = &self.build { DevContainerBuildType::Dockerfile(build.clone()) } else { DevContainerBuildType::None } } pub(crate) fn override_command(&self) -> bool { self.override_command.unwrap_or(!matches!( self.build_type(), DevContainerBuildType::DockerCompose )) } pub(crate) fn validate_devcontainer_contents(&self) -> Result<(), DevContainerError> { match self.build_type() { DevContainerBuildType::Image(_) => Ok(()), DevContainerBuildType::Dockerfile(_) => { if (self.workspace_folder.is_some() && self.workspace_mount.is_none()) || (self.workspace_folder.is_none() && self.workspace_mount.is_some()) { return Err(DevContainerError::DevContainerValidationFailed( "workspaceMount and workspaceFolder must both be defined, or neither defined" .to_string(), )); } Ok(()) } DevContainerBuildType::DockerCompose => { if self.service.is_none() { return Err(DevContainerError::DevContainerValidationFailed( "must specify a connecting service for docker-compose".to_string(), )); } Ok(()) } DevContainerBuildType::None => Ok(()), } }} // Custom deserializer that parses the entire customizations object as a// serde_json_lenient::Value first, then extracts the "zed" portion.// This avoids a bug in serde_json_lenient's `ignore_value` codepath which// does not handle trailing commas in skipped values.impl<'de> Deserialize<'de> for ZedCustomizationsWrapper { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { let value = Value::deserialize(deserializer)?; let zed = value .get("zed") .map(|zed_value| serde_json_lenient::from_value::<ZedCustomization>(zed_value.clone())) .transpose() .map_err(serde::de::Error::custom)? .unwrap_or_default(); Ok(ZedCustomizationsWrapper { zed }) }} impl LifecycleScript { fn from_map(args: HashMap<String, Vec<String>>) -> Self { Self { scripts: args .into_iter() .map(|(k, v)| (k, LifecycleScriptInternal::from_args(v))) .collect(), } } fn shell_command(script: &str) -> Vec<String> { vec!["/bin/sh".to_string(), "-c".to_string(), script.to_string()] } fn from_str(args: &str) -> Self { Self::from_args(Self::shell_command(args)) } fn from_args(args: Vec<String>) -> Self { Self::from_map(HashMap::from([("default".to_string(), args)])) } pub fn script_commands(&self) -> HashMap<String, Command> { self.scripts .iter() .filter_map(|(k, v)| { if let Some(inner_command) = &v.command { let mut command = Command::new(inner_command); command.args(&v.args); Some((k.clone(), command)) } else { log::warn!( "Lifecycle script command {k}, value {:?} has no program to run. Skipping", v ); None } }) .collect() } pub async fn run( &self, command_runnder: &Arc<dyn CommandRunner>, working_directory: &Path, ) -> Result<(), DevContainerError> { for (command_name, mut command) in self.script_commands() { log::debug!("Running script {command_name}"); command.current_dir(working_directory); let output = command_runnder .run_command(&mut command) .await .map_err(|e| { log::error!("Error running command {command_name}: {e}"); DevContainerError::CommandFailed(command_name.clone()) })?; if !output.status.success() { let std_err = String::from_utf8_lossy(&output.stderr); log::error!( "Command {command_name} produced a non-successful output. StdErr: {std_err}" ); } let std_out = String::from_utf8_lossy(&output.stdout); log::debug!("Command {command_name} output:\n {std_out}"); } Ok(()) }} impl<'de> Deserialize<'de> for LifecycleScript { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { use serde::de::{self, Visitor}; use std::fmt; struct LifecycleScriptVisitor; impl<'de> Visitor<'de> for LifecycleScriptVisitor { type Value = LifecycleScript; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a string, an array of strings, or a map of arrays") } fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> where E: de::Error, { Ok(LifecycleScript::from_str(value)) } fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error> where A: de::SeqAccess<'de>, { let mut array = Vec::new(); while let Some(elem) = seq.next_element()? { array.push(elem); } Ok(LifecycleScript::from_args(array)) } fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error> where A: de::MapAccess<'de>, { let mut result = HashMap::new(); while let Some(key) = map.next_key::<String>()? { let value: Value = map.next_value()?; let script_args = match value { Value::String(s) => LifecycleScript::shell_command(&s), Value::Array(arr) => { let strings: Vec<String> = arr .into_iter() .filter_map(|v| v.as_str().map(|s| s.to_string())) .collect(); strings } _ => continue, }; result.insert(key, script_args); } Ok(LifecycleScript::from_map(result)) } } deserializer.deserialize_any(LifecycleScriptVisitor) }} fn deserialize_mount_definition<'de, D>( deserializer: D,) -> Result<Option<MountDefinition>, D::Error>where D: serde::Deserializer<'de>,{ use serde::Deserialize; use serde::de::Error; #[derive(Deserialize)] #[serde(untagged)] enum MountItem { Object(MountDefinition), String(String), } let item = MountItem::deserialize(deserializer)?; let mount = match item { MountItem::Object(mount) => mount, MountItem::String(s) => { let mut source = None; let mut target = None; let mut mount_type = None; for part in s.split(',') { let part = part.trim(); if let Some((key, value)) = part.split_once('=') { match key.trim() { "source" => source = Some(value.trim().to_string()), "target" => target = Some(value.trim().to_string()), "type" => mount_type = Some(value.trim().to_string()), _ => {} // Ignore unknown keys } } }Lines 1–500 of 1839. Load more
use std::{collections::HashMap, fmt::Display, path::Path, sync::Arc}; use crate::{command_json::CommandRunner, devcontainer_api::DevContainerError}; use serde::{Deserialize, Deserializer, Serialize}; use serde_json_lenient::Value; use util::command::Command; #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] #[serde(untagged)] pub(crate) enum ForwardPort { Number(u16), String(String), } #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub(crate) enum PortAttributeProtocol { Https, Http, } #[derive(Clone, Debug, Default, Deserialize, Serialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub(crate) enum OnAutoForward { #[default] Notify, OpenBrowser, OpenBrowserOnce, OpenPreview, Silent, Ignore, } #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub(crate) struct PortAttributes { #[serde(default)] label: Option<String>, #[serde(default)] on_auto_forward: OnAutoForward, #[serde(default)] elevate_if_needed: bool, #[serde(default)] require_local_port: bool, #[serde(default)] protocol: Option<PortAttributeProtocol>, } #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub(crate) enum UserEnvProbe { None, InteractiveShell, LoginShell, LoginInteractiveShell, } #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub(crate) enum ShutdownAction { None, StopContainer, StopCompose, } #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub(crate) struct MountDefinition { #[serde(default)] pub(crate) source: Option<String>, pub(crate) target: String, #[serde(rename = "type")] pub(crate) mount_type: Option<String>, } impl Display for MountDefinition { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mount_type = self.mount_type.clone().unwrap_or_else(|| { if let Some(source) = &self.source { if source.starts_with('/') || source.starts_with("\\\\") || source.get(1..3) == Some(":\\") || source.get(1..3) == Some(":/") { return "bind".to_string(); } } "volume".to_string() }); write!(f, "type={}", mount_type)?; if let Some(source) = &self.source { write!(f, ",source={}", source)?; } write!(f, ",target={},consistency=cached", self.target) } } /// Represents the value associated with a feature ID in the `features` map of devcontainer.json. /// /// Per the spec, the value can be: /// - A boolean (`true` to enable with defaults) /// - A string (shorthand for `{"version": "<value>"}`) /// - An object mapping option names to string or boolean values /// /// See: https://containers.dev/implementors/features/#devcontainerjson-properties #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] #[serde(untagged)] pub(crate) enum FeatureOptions { Bool(bool), String(String), Options(HashMap<String, FeatureOptionValue>), } #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] #[serde(untagged)] pub(crate) enum FeatureOptionValue { Bool(bool), String(String), } impl std::fmt::Display for FeatureOptionValue { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { FeatureOptionValue::Bool(b) => write!(f, "{}", b), FeatureOptionValue::String(s) => write!(f, "{}", s), } } } #[derive(Clone, Debug, Serialize, Eq, PartialEq, Default)] pub(crate) struct ZedCustomizationsWrapper { pub(crate) zed: ZedCustomization, } #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq, Default)] pub(crate) struct ZedCustomization { #[serde(default)] pub(crate) extensions: Vec<String>, } #[derive(Debug, Clone, Deserialize, Serialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub(crate) struct ContainerBuild { pub(crate) dockerfile: String, pub(crate) context: Option<String>, pub(crate) args: Option<HashMap<String, String>>, pub(crate) options: Option<Vec<String>>, pub(crate) target: Option<String>, #[serde(default, deserialize_with = "deserialize_string_or_array")] pub(crate) cache_from: Option<Vec<String>>, } #[derive(Clone, Debug, Serialize, Eq, PartialEq)] struct LifecycleScriptInternal { command: Option<String>, args: Vec<String>, } impl LifecycleScriptInternal { fn from_args(args: Vec<String>) -> Self { let command = args.get(0).map(|a| a.to_string()); let remaining = args.iter().skip(1).map(|a| a.to_string()).collect(); Self { command, args: remaining, } } } #[derive(Clone, Debug, Serialize, Eq, PartialEq)] pub struct LifecycleScript { scripts: HashMap<String, LifecycleScriptInternal>, } #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub(crate) struct HostRequirements { cpus: Option<u16>, memory: Option<String>, storage: Option<String>, } #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub(crate) enum LifecycleCommand { InitializeCommand, OnCreateCommand, UpdateContentCommand, PostCreateCommand, PostStartCommand, } #[derive(Debug, PartialEq, Eq)] pub(crate) enum DevContainerBuildType { Image(String), Dockerfile(ContainerBuild), DockerCompose, None, } #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq, Default)] #[serde(rename_all = "camelCase")] pub(crate) struct DevContainer { pub(crate) image: Option<String>, pub(crate) name: Option<String>, pub(crate) remote_user: Option<String>, pub(crate) forward_ports: Option<Vec<ForwardPort>>, pub(crate) ports_attributes: Option<HashMap<String, PortAttributes>>, pub(crate) other_ports_attributes: Option<PortAttributes>, pub(crate) container_env: Option<HashMap<String, String>>, pub(crate) remote_env: Option<HashMap<String, String>>, pub(crate) container_user: Option<String>, #[serde(rename = "updateRemoteUserUID")] pub(crate) update_remote_user_uid: Option<bool>, user_env_probe: Option<UserEnvProbe>, pub(crate) override_command: Option<bool>, shutdown_action: Option<ShutdownAction>, pub(crate) init: Option<bool>, pub(crate) privileged: Option<bool>, pub(crate) cap_add: Option<Vec<String>>, pub(crate) security_opt: Option<Vec<String>>, #[serde(default, deserialize_with = "deserialize_mount_definitions")] pub(crate) mounts: Option<Vec<MountDefinition>>, pub(crate) features: Option<HashMap<String, FeatureOptions>>, pub(crate) override_feature_install_order: Option<Vec<String>>, pub(crate) customizations: Option<ZedCustomizationsWrapper>, pub(crate) build: Option<ContainerBuild>, #[serde(default, deserialize_with = "deserialize_app_port")] pub(crate) app_port: Vec<String>, #[serde(default, deserialize_with = "deserialize_mount_definition")] pub(crate) workspace_mount: Option<MountDefinition>, pub(crate) workspace_folder: Option<String>, pub(crate) run_args: Option<Vec<String>>, #[serde(default, deserialize_with = "deserialize_string_or_array")] pub(crate) docker_compose_file: Option<Vec<String>>, pub(crate) service: Option<String>, pub(crate) run_services: Option<Vec<String>>, pub(crate) initialize_command: Option<LifecycleScript>, pub(crate) on_create_command: Option<LifecycleScript>, pub(crate) update_content_command: Option<LifecycleScript>, pub(crate) post_create_command: Option<LifecycleScript>, pub(crate) post_start_command: Option<LifecycleScript>, pub(crate) post_attach_command: Option<LifecycleScript>, wait_for: Option<LifecycleCommand>, host_requirements: Option<HostRequirements>, } pub(crate) fn deserialize_devcontainer_json_to_value( json: &str, ) -> Result<serde_json_lenient::Value, DevContainerError> { serde_json_lenient::from_str(json).map_err(|e| { log::error!("Unable to deserialize json values: {e}"); DevContainerError::DevContainerParseFailed }) } pub(crate) fn deserialize_devcontainer_json_from_value( json: serde_json_lenient::Value, ) -> Result<DevContainer, DevContainerError> { serde_json_lenient::from_value(json).map_err(|e| { log::error!("Unable to deserialize devcontainer from json values: {e}"); DevContainerError::DevContainerParseFailed }) } pub(crate) fn deserialize_devcontainer_json(json: &str) -> Result<DevContainer, DevContainerError> { deserialize_devcontainer_json_to_value(json).and_then(deserialize_devcontainer_json_from_value) } impl DevContainer { pub(crate) fn build_type(&self) -> DevContainerBuildType { if let Some(image) = &self.image { DevContainerBuildType::Image(image.clone()) } else if self.docker_compose_file.is_some() { DevContainerBuildType::DockerCompose } else if let Some(build) = &self.build { DevContainerBuildType::Dockerfile(build.clone()) } else { DevContainerBuildType::None } } pub(crate) fn override_command(&self) -> bool { self.override_command.unwrap_or(!matches!( self.build_type(), DevContainerBuildType::DockerCompose )) } pub(crate) fn validate_devcontainer_contents(&self) -> Result<(), DevContainerError> { match self.build_type() { DevContainerBuildType::Image(_) => Ok(()), DevContainerBuildType::Dockerfile(_) => { if (self.workspace_folder.is_some() && self.workspace_mount.is_none()) || (self.workspace_folder.is_none() && self.workspace_mount.is_some()) { return Err(DevContainerError::DevContainerValidationFailed( "workspaceMount and workspaceFolder must both be defined, or neither defined" .to_string(), )); } Ok(()) } DevContainerBuildType::DockerCompose => { if self.service.is_none() { return Err(DevContainerError::DevContainerValidationFailed( "must specify a connecting service for docker-compose".to_string(), )); } Ok(()) } DevContainerBuildType::None => Ok(()), } } } // Custom deserializer that parses the entire customizations object as a // serde_json_lenient::Value first, then extracts the "zed" portion. // This avoids a bug in serde_json_lenient's `ignore_value` codepath which // does not handle trailing commas in skipped values. impl<'de> Deserialize<'de> for ZedCustomizationsWrapper { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { let value = Value::deserialize(deserializer)?; let zed = value .get("zed") .map(|zed_value| serde_json_lenient::from_value::<ZedCustomization>(zed_value.clone())) .transpose() .map_err(serde::de::Error::custom)? .unwrap_or_default(); Ok(ZedCustomizationsWrapper { zed }) } } impl LifecycleScript { fn from_map(args: HashMap<String, Vec<String>>) -> Self { Self { scripts: args .into_iter() .map(|(k, v)| (k, LifecycleScriptInternal::from_args(v))) .collect(), } } fn shell_command(script: &str) -> Vec<String> { vec!["/bin/sh".to_string(), "-c".to_string(), script.to_string()] } fn from_str(args: &str) -> Self { Self::from_args(Self::shell_command(args)) } fn from_args(args: Vec<String>) -> Self { Self::from_map(HashMap::from([("default".to_string(), args)])) } pub fn script_commands(&self) -> HashMap<String, Command> { self.scripts .iter() .filter_map(|(k, v)| { if let Some(inner_command) = &v.command { let mut command = Command::new(inner_command); command.args(&v.args); Some((k.clone(), command)) } else { log::warn!( "Lifecycle script command {k}, value {:?} has no program to run. Skipping", v ); None } }) .collect() } pub async fn run( &self, command_runnder: &Arc<dyn CommandRunner>, working_directory: &Path, ) -> Result<(), DevContainerError> { for (command_name, mut command) in self.script_commands() { log::debug!("Running script {command_name}"); command.current_dir(working_directory); let output = command_runnder .run_command(&mut command) .await .map_err(|e| { log::error!("Error running command {command_name}: {e}"); DevContainerError::CommandFailed(command_name.clone()) })?; if !output.status.success() { let std_err = String::from_utf8_lossy(&output.stderr); log::error!( "Command {command_name} produced a non-successful output. StdErr: {std_err}" ); } let std_out = String::from_utf8_lossy(&output.stdout); log::debug!("Command {command_name} output:\n {std_out}"); } Ok(()) } } impl<'de> Deserialize<'de> for LifecycleScript { fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> where D: Deserializer<'de>, { use serde::de::{self, Visitor}; use std::fmt; struct LifecycleScriptVisitor; impl<'de> Visitor<'de> for LifecycleScriptVisitor { type Value = LifecycleScript; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a string, an array of strings, or a map of arrays") } fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> where E: de::Error, { Ok(LifecycleScript::from_str(value)) } fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error> where A: de::SeqAccess<'de>, { let mut array = Vec::new(); while let Some(elem) = seq.next_element()? { array.push(elem); } Ok(LifecycleScript::from_args(array)) } fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error> where A: de::MapAccess<'de>, { let mut result = HashMap::new(); while let Some(key) = map.next_key::<String>()? { let value: Value = map.next_value()?; let script_args = match value { Value::String(s) => LifecycleScript::shell_command(&s), Value::Array(arr) => { let strings: Vec<String> = arr .into_iter() .filter_map(|v| v.as_str().map(|s| s.to_string())) .collect(); strings } _ => continue, }; result.insert(key, script_args); } Ok(LifecycleScript::from_map(result)) } } deserializer.deserialize_any(LifecycleScriptVisitor) } } fn deserialize_mount_definition<'de, D>( deserializer: D, ) -> Result<Option<MountDefinition>, D::Error> where D: serde::Deserializer<'de>, { use serde::Deserialize; use serde::de::Error; #[derive(Deserialize)] #[serde(untagged)] enum MountItem { Object(MountDefinition), String(String), } let item = MountItem::deserialize(deserializer)?; let mount = match item { MountItem::Object(mount) => mount, MountItem::String(s) => { let mut source = None; let mut target = None; let mut mount_type = None; for part in s.split(',') { let part = part.trim(); if let Some((key, value)) = part.split_once('=') { match key.trim() { "source" => source = Some(value.trim().to_string()), "target" => target = Some(value.trim().to_string()), "type" => mount_type = Some(value.trim().to_string()), _ => {} // Ignore unknown keys } } }