use color_eyre::eyre::{Result, WrapErr, bail, eyre};
use log::{debug, trace};
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::process::Command;
use walkdir::WalkDir;
mod open;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DevContainer {
pub config_path: PathBuf,
pub name: Option<String>,
pub workspace_path_in_container: String,
}
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}");
Self::substitute_variables(folder, workspace_name)
} 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 path_log = path.display();
let content = std::fs::read_to_string(path)
.wrap_err_with(|| format!("Failed to read dev container config file: {path_log}"))?;
let config: serde_json::Value = json5::from_str(&content)
.wrap_err_with(|| format!("Failed to parse json file: {path_log}"))?;
debug!("Parsed dev container config: {path_log}");
Ok(config)
}
fn substitute_variables(folder: &str, workspace_name: &str) -> String {
let mut result = folder.to_owned();
result = result.replace("${localWorkspaceFolderBasename}", workspace_name);
let default_workspace = format!("/workspaces/{workspace_name}");
result = result.replace("${localWorkspaceFolder}", &default_workspace);
result
}
}
#[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_log = path.display();
let path = std::fs::canonicalize(path)
.wrap_err_with(|| format!("Error canonicalizing path: {path_log}"))?;
trace!("Canonicalized path: {path_log}");
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_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(unix)]
pub fn resolve_local_path(path: &str) -> String {
if (path.starts_with("\\\\wsl") || path.starts_with("//wsl"))
&& let Ok(converted) =
wslpath2::convert(path, None, wslpath2::Conversion::WindowsToWsl, true)
{
return converted;
}
path.to_string()
}
#[cfg(windows)]
pub fn resolve_local_path(path: &str) -> String {
path.to_string()
}
#[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"
);
}
#[test]
fn test_deserialize_devcontainer_with_comments_and_trailing_commas() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("devcontainer.json");
std::fs::write(
&path,
r#"{
// JSON comments are supported by devcontainer files.
"name": "Rust",
"features": {
"ghcr.io/guiyomh/features/just:0": {},
},
}"#,
)
.unwrap();
let dev_container = DevContainer::from_config(&path, "test").unwrap();
assert_eq!(dev_container.name, Some(String::from("Rust")));
}
#[test]
fn test_substitute_variables() {
let folder = "/workspaces/${localWorkspaceFolderBasename}";
let result = DevContainer::substitute_variables(folder, "my-project");
assert_eq!(result, "/workspaces/my-project");
let folder = "${localWorkspaceFolder}/src";
let result = DevContainer::substitute_variables(folder, "my-project");
assert_eq!(result, "/workspaces/my-project/src");
let folder = "${localWorkspaceFolder}/${localWorkspaceFolderBasename}-test";
let result = DevContainer::substitute_variables(folder, "my-project");
assert_eq!(result, "/workspaces/my-project/my-project-test");
let folder = "/custom/path";
let result = DevContainer::substitute_variables(folder, "my-project");
assert_eq!(result, "/custom/path");
}
}