use color_eyre::eyre::{Result, WrapErr, bail, eyre};
use log::{debug, trace};
use std::ffi::OsString;
use std::fmt::Display;
use std::path::{Path, PathBuf};
use std::process::Command;
use walkdir::WalkDir;
use crate::uri::{DevcontainerUriJson, FileUriJson};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DevContainer {
pub config_path: PathBuf,
pub name: Option<String>,
pub workspace_path_in_container: String,
}
impl Display for DevContainer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let path = self.config_path.display();
if let Some(name) = &self.name {
write!(f, "{name} ({path})")
} else {
write!(f, "{path}")
}
}
}
impl DevContainer {
pub fn from_config(path: &Path, workspace_name: &str) -> Result<DevContainer> {
let dev_container = Self::parse_dev_container_config(path)?;
trace!("dev container config: {:?}", dev_container);
let folder: String = if let Some(folder) = dev_container["workspaceFolder"].as_str() {
debug!("Read workspace folder from config: {}", folder);
folder.to_owned()
} else {
debug!("Could not read workspace folder from config -> using default folder");
format!("/workspaces/{workspace_name}")
};
trace!("Workspace folder: {folder}");
let name = if let Some(name) = dev_container["name"].as_str() {
debug!("Read workspace name from config: {}", name);
Some(name.to_owned())
} else {
debug!("Could not read workspace name from config");
None
};
trace!("Workspace name: {name:?}");
Ok(DevContainer {
config_path: path.to_owned(),
workspace_path_in_container: folder,
name,
})
}
fn parse_dev_container_config(path: &Path) -> Result<serde_json::Value> {
let content = std::fs::read_to_string(path)
.wrap_err_with(|| format!("Failed to read dev container config file: {path:?}"))?;
let config: serde_json::Value = json5::from_str(&content)
.wrap_err_with(|| format!("Failed to parse json file: {path:?}"))?;
debug!("Parsed dev container config: {:?}", path);
Ok(config)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Workspace {
pub path: PathBuf,
pub name: String,
}
impl Workspace {
pub fn from_path(path: &Path) -> Result<Workspace> {
if !path.exists() {
bail!("Path {} does not exist", path.display());
}
let path = std::fs::canonicalize(path)
.wrap_err_with(|| format!("Error canonicalizing path: {path:?}"))?;
trace!("Canonicalized path: {}", path.display());
let workspace_name = path
.file_name()
.ok_or_else(|| eyre!("Error getting workspace from path"))?
.to_string_lossy()
.into_owned();
trace!("Workspace name: {workspace_name}");
let ws = Workspace {
path,
name: workspace_name,
};
trace!("{ws:?}");
Ok(ws)
}
pub fn find_dev_container_configs(&self) -> Vec<PathBuf> {
let mut configs = Vec::new();
let direct_config = self.path.join(".devcontainer.json");
if direct_config.is_file() {
trace!("Found dev container config: {}", direct_config.display());
configs.push(direct_config);
}
let dev_container_dir = self.path.join(".devcontainer");
for entry in WalkDir::new(dev_container_dir)
.max_depth(2)
.sort_by_file_name()
.into_iter()
.filter(|e| matches!(e, Ok(x) if x.file_type().is_file() && x.file_name() == "devcontainer.json"))
.flatten()
{
let path = entry.into_path();
trace!(
"Found dev container config in .devcontainer folder: {}",
path.display()
);
configs.push(path);
}
debug!(
"Found {} dev container configs: {:?}",
configs.len(),
configs
);
configs
}
pub fn load_dev_containers(&self, paths: &[PathBuf]) -> Result<Vec<DevContainer>> {
paths
.iter()
.map(|config_path| DevContainer::from_config(config_path, &self.name))
.collect::<Result<Vec<_>, _>>()
}
pub fn open(
&self,
mut args: Vec<OsString>,
dry_run: bool,
dev_container: &DevContainer,
command: &str,
) -> Result<()> {
if args.iter().any(|arg| arg == "--folder-uri") {
bail!("Specifying `--folder-uri` is not possible while using vscli.");
}
let container_folder: String = dev_container.workspace_path_in_container.clone();
let mut ws_path: String = self.path.to_string_lossy().into_owned();
let mut dc_path: String = dev_container.config_path.to_string_lossy().into_owned();
let is_wsl: bool = {
#[cfg(unix)]
{
let output = Command::new("uname")
.arg("-a")
.output()
.expect("Failed to execute command");
let uname_output = String::from_utf8(output.stdout)?;
(uname_output.contains("Microsoft") || uname_output.contains("WSL"))
&& std::env::var("WSLENV").is_ok()
}
#[cfg(windows)]
{
false
}
};
if is_wsl {
debug!("WSL detected");
ws_path = wslpath2::convert(
ws_path.as_str(),
None,
wslpath2::Conversion::WslToWindows,
true,
)
.map_err(|e| eyre!("Error while getting wslpath: {} (path: {ws_path:?})", e))?;
dc_path = wslpath2::convert(
dc_path.as_str(),
None,
wslpath2::Conversion::WslToWindows,
true,
)
.map_err(|e| eyre!("Error while getting wslpath: {} (path: {dc_path:?})", e))?;
}
#[cfg(windows)]
{
ws_path = ws_path.replace("\\\\?\\", "");
dc_path = dc_path.replace("\\\\?\\", "");
}
let folder_uri = DevcontainerUriJson {
host_path: ws_path,
config_file: FileUriJson::new(dc_path.as_str()),
};
let json = serde_json::to_string(&folder_uri)?;
trace!("Folder uri JSON: {json}");
let hex = hex::encode(json.as_bytes());
let uri = format!("vscode-remote://dev-container+{hex}{container_folder}");
args.push(OsString::from("--folder-uri"));
args.push(OsString::from(uri.as_str()));
exec_code(args, dry_run, command)
.wrap_err_with(|| "Error opening vscode using dev container...")
}
pub fn open_classic(
&self,
mut args: Vec<OsString>,
dry_run: bool,
command: &str,
) -> Result<()> {
trace!("path: {}", self.path.display());
trace!("args: {:?}", args);
args.insert(0, self.path.as_os_str().to_owned());
exec_code(args, dry_run, command)
.wrap_err_with(|| "Error opening vscode the classic way...")
}
}
#[cfg(unix)]
fn exec_code(args: Vec<OsString>, dry_run: bool, command: &str) -> Result<()> {
Command::new(command)
.arg("-v")
.output()
.wrap_err_with(|| format!("`{command}` does not exists."))?;
run(command, args, dry_run)
}
#[cfg(windows)]
fn exec_code(mut args: Vec<OsString>, dry_run: bool, command: &str) -> Result<()> {
let cmd = "cmd";
args.insert(0, OsString::from("/c"));
args.insert(1, OsString::from(command));
Command::new(cmd)
.arg("-v")
.output()
.wrap_err_with(|| format!("`{cmd}` does not exists."))?;
run(cmd, args, dry_run)
}
fn run(cmd: &str, args: Vec<OsString>, dry_run: bool) -> Result<()> {
debug!("executable: {}", cmd);
debug!("final args: {:?}", args);
if !dry_run {
let output = Command::new(cmd).args(args).output()?;
debug!("Command output: {:?}", output);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_deserialize_devcontainer() {
let path = PathBuf::from("tests/fixtures/devcontainer.json");
let result = DevContainer::from_config(&path, "test");
assert!(result.is_ok());
let dev_container = result.unwrap();
assert_eq!(dev_container.config_path, path);
assert_eq!(dev_container.name, Some(String::from("Rust")));
assert_eq!(
dev_container.workspace_path_in_container,
"/workspaces/test"
);
}
}