Skip to content

Commit f144538

Browse files
committed
Dev containers native implementation (zed-industries#52338)
## Context Closes zed-industries#11473 In-house Zed implementation of devcontainers. Replaces the dependency on the [reference implementation](https://github.com/devcontainers/cli) via Node. This enables additional features with this implementation: 1. Zed extensions can be specified in the `customizations` block, via this syntax in `devcontainer.json: ``` ... "customizations": { "zed": { "extensions": ["vue", "ruby"], }, }, ``` 2. [forwardPorts](https://containers.dev/implementors/json_reference/#general-properties) are supported for multiple ports proxied to the host ## How to Review <!-- Help reviewers focus their attention: - For small PRs: note what to focus on (e.g., "error handling in foo.rs") - For large PRs (>400 LOC): provide a guided tour — numbered list of files/commits to read in order. (The `large-pr` label is applied automatically.) - See the review process guidelines for comment conventions --> ## Self-Review Checklist <!-- Check before requesting review: --> - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - Improved devcontainer implementation by moving initialization and creation in-house
1 parent f22d169 commit f144538

18 files changed

Lines changed: 10076 additions & 662 deletions

File tree

Cargo.lock

Lines changed: 6 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/dev_container/Cargo.toml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,26 @@ publish.workspace = true
55
edition.workspace = true
66

77
[dependencies]
8+
async-tar.workspace = true
9+
async-trait.workspace = true
810
serde.workspace = true
911
serde_json.workspace = true
12+
serde_json_lenient.workspace = true
13+
shlex.workspace = true
1014
http_client.workspace = true
1115
http.workspace = true
1216
gpui.workspace = true
17+
fs.workspace = true
1318
futures.workspace = true
1419
log.workspace = true
15-
node_runtime.workspace = true
1620
menu.workspace = true
1721
paths.workspace = true
1822
picker.workspace = true
23+
project.workspace = true
1924
settings.workspace = true
20-
smol.workspace = true
2125
ui.workspace = true
2226
util.workspace = true
27+
walkdir.workspace = true
2328
worktree.workspace = true
2429
workspace.workspace = true
2530

@@ -32,6 +37,8 @@ settings = { workspace = true, features = ["test-support"] }
3237

3338
workspace = { workspace = true, features = ["test-support"] }
3439
worktree = { workspace = true, features = ["test-support"] }
40+
util = { workspace = true, features = ["test-support"] }
41+
env_logger.workspace = true
3542

3643
[lints]
37-
workspace = true
44+
workspace = true
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use std::process::Output;
2+
3+
use async_trait::async_trait;
4+
use serde::Deserialize;
5+
use util::command::Command;
6+
7+
use crate::devcontainer_api::DevContainerError;
8+
9+
pub(crate) struct DefaultCommandRunner;
10+
11+
impl DefaultCommandRunner {
12+
pub(crate) fn new() -> Self {
13+
Self
14+
}
15+
}
16+
17+
#[async_trait]
18+
impl CommandRunner for DefaultCommandRunner {
19+
async fn run_command(&self, command: &mut Command) -> Result<Output, std::io::Error> {
20+
command.output().await
21+
}
22+
}
23+
24+
#[async_trait]
25+
pub(crate) trait CommandRunner: Send + Sync {
26+
async fn run_command(&self, command: &mut Command) -> Result<Output, std::io::Error>;
27+
}
28+
29+
pub(crate) async fn evaluate_json_command<T>(
30+
mut command: Command,
31+
) -> Result<Option<T>, DevContainerError>
32+
where
33+
T: for<'de> Deserialize<'de>,
34+
{
35+
let output = command.output().await.map_err(|e| {
36+
log::error!("Error running command {:?}: {e}", command);
37+
DevContainerError::CommandFailed(command.get_program().display().to_string())
38+
})?;
39+
40+
deserialize_json_output(output).map_err(|e| {
41+
log::error!("Error running command {:?}: {e}", command);
42+
DevContainerError::CommandFailed(command.get_program().display().to_string())
43+
})
44+
}
45+
46+
pub(crate) fn deserialize_json_output<T>(output: Output) -> Result<Option<T>, String>
47+
where
48+
T: for<'de> Deserialize<'de>,
49+
{
50+
if output.status.success() {
51+
let raw = String::from_utf8_lossy(&output.stdout);
52+
if raw.is_empty() || raw.trim() == "[]" || raw.trim() == "{}" {
53+
return Ok(None);
54+
}
55+
let value = serde_json_lenient::from_str(&raw)
56+
.map_err(|e| format!("Error deserializing from raw json: {e}"));
57+
value
58+
} else {
59+
let std_err = String::from_utf8_lossy(&output.stderr);
60+
Err(format!(
61+
"Sent non-successful output; cannot deserialize. StdErr: {std_err}"
62+
))
63+
}
64+
}

0 commit comments

Comments
 (0)