Skip to content

Commit 6b618b3

Browse files
committed
Move devenv generate to a separate binary
1 parent 4c86a98 commit 6b618b3

File tree

9 files changed

+316
-149
lines changed

9 files changed

+316
-149
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
resolver = "2"
33
members = [
44
"devenv",
5+
"devenv-generate",
56
"devenv-eval-cache",
67
"devenv-run-tests",
78
"devenv-tasks",
@@ -24,6 +25,7 @@ nix-conf-parser = { path = "nix-conf-parser" }
2425
xtask = { path = "xtask" }
2526

2627
ansiterm = "0.12.2"
28+
binaryornot = "1.0.0"
2729
blake3 = "1.5.4"
2830
clap = { version = "4.5.1", features = ["derive", "cargo", "env"] }
2931
cli-table = "0.4.7"
@@ -38,6 +40,7 @@ indoc = "2.0.4"
3840
lazy_static = "1.5.0"
3941
miette = { version = "7.1.0", features = ["fancy"] }
4042
nix = { version = "0.28.0", features = ["signal"] }
43+
once_cell = "1.20.2"
4144
petgraph = "0.6.5"
4245
pretty_assertions = { version = "1.4.0", features = ["unstable"] }
4346
regex = "1.10.3"
@@ -53,6 +56,7 @@ serde = { version = "1.0.197", features = ["derive"] }
5356
serde_json = "1.0.114"
5457
serde_repr = "0.1.19"
5558
serde_yaml = "0.9.32"
59+
similar = "2.6.0"
5660
sha2 = "0.10.8"
5761
sqlx = { version = "0.8.2", features = ["time", "sqlite", "runtime-tokio"] }
5862
tempdir = "0.3.7"
@@ -70,6 +74,7 @@ tokio = { version = "1.39.3", features = [
7074
"sync",
7175
"time",
7276
] }
77+
tokio-util = { version = "0.7.12", features = ["io"] }
7378
which = "6.0.0"
7479
whoami = "1.5.1"
7580
xdg = "2.5.2"

devenv-generate/Cargo.toml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[package]
2+
name = "devenv-generate"
3+
version = "1.4.1"
4+
edition.workspace = true
5+
license.workspace = true
6+
7+
[dependencies]
8+
devenv.workspace = true
9+
10+
clap = { workspace = true, features = ["derive"] }
11+
console.workspace = true
12+
dialoguer.workspace = true
13+
indoc.workspace = true
14+
miette.workspace = true
15+
reqwest.workspace = true
16+
serde.workspace = true
17+
serde_json.workspace = true
18+
tokio = { workspace = true, features = ["full"] }
19+
tokio-tar.workspace = true
20+
tracing.workspace = true
21+
tokio-util.workspace = true
22+
similar.workspace = true
23+
binaryornot.workspace = true
24+
once_cell.workspace = true

devenv-generate/src/main.rs

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
use clap::{crate_version, Parser};
2+
use devenv::{
3+
default_system,
4+
log::{self, LogFormat},
5+
};
6+
use miette::{bail, IntoDiagnostic, Result};
7+
use similar::{ChangeTag, TextDiff};
8+
use std::path::{Path, PathBuf};
9+
use tracing::{info, warn};
10+
11+
#[derive(Parser, Debug)]
12+
#[command(
13+
name = "devenv-generate",
14+
about = "Generate devenv.yaml and devenv.nix using AI"
15+
)]
16+
struct Cli {
17+
#[arg(num_args=0.., trailing_var_arg = true)]
18+
description: Vec<String>,
19+
20+
#[clap(long, default_value = "https://devenv.new")]
21+
host: String,
22+
23+
#[arg(
24+
long,
25+
help = "Paths to exclude during generation.",
26+
value_name = "PATH"
27+
)]
28+
exclude: Vec<PathBuf>,
29+
30+
// https://consoledonottrack.com/
31+
#[clap(long, env = "DO_NOT_TRACK", action = clap::ArgAction::SetTrue)]
32+
disable_telemetry: bool,
33+
34+
#[arg(
35+
short = 'V',
36+
long,
37+
global = true,
38+
help = "Print version information",
39+
long_help = "Print version information and exit"
40+
)]
41+
pub version: bool,
42+
43+
#[arg(short, long, global = true, default_value_t = default_system())]
44+
pub system: String,
45+
46+
#[arg(short, long, global = true, help = "Enable additional debug logs.")]
47+
verbose: bool,
48+
49+
#[arg(
50+
short,
51+
long,
52+
global = true,
53+
conflicts_with = "verbose",
54+
help = "Silence all logs"
55+
)]
56+
pub quiet: bool,
57+
58+
#[arg(
59+
long,
60+
global = true,
61+
help = "Configure the output format of the logs.",
62+
default_value_t,
63+
value_enum
64+
)]
65+
pub log_format: LogFormat,
66+
}
67+
68+
#[derive(serde::Deserialize)]
69+
struct GenerateResponse {
70+
devenv_nix: String,
71+
devenv_yaml: String,
72+
}
73+
74+
#[tokio::main]
75+
async fn main() -> Result<()> {
76+
let cli = Cli::parse();
77+
78+
if cli.version {
79+
println!("devenv {} ({})", crate_version!(), cli.system);
80+
return Ok(());
81+
}
82+
83+
let level = if cli.verbose {
84+
log::Level::Debug
85+
} else if cli.quiet {
86+
log::Level::Silent
87+
} else {
88+
log::Level::default()
89+
};
90+
91+
log::init_tracing(level, cli.log_format);
92+
93+
let description = if !cli.description.is_empty() {
94+
Some(cli.description.join(" "))
95+
} else {
96+
None
97+
};
98+
99+
let client = reqwest::Client::new();
100+
let mut request = client
101+
.post(&cli.host)
102+
.query(&[("disable_telemetry", cli.disable_telemetry)])
103+
.header(reqwest::header::USER_AGENT, crate_version!());
104+
105+
let (asyncwriter, asyncreader) = tokio::io::duplex(256 * 1024);
106+
let streamreader = tokio_util::io::ReaderStream::new(asyncreader);
107+
108+
let (body_sender, body) = match description {
109+
Some(desc) => {
110+
request = request.query(&[("q", desc)]);
111+
(None, None)
112+
}
113+
None => {
114+
let git_output = std::process::Command::new("git")
115+
.args(["ls-files", "-z"])
116+
.output()
117+
.map_err(|_| miette::miette!("Failed to get list of files from git ls-files"))?;
118+
119+
let files = String::from_utf8_lossy(&git_output.stdout)
120+
.split('\0')
121+
.filter(|s| !s.is_empty())
122+
.filter(|s| !binaryornot::is_binary(s).unwrap_or(false))
123+
.map(PathBuf::from)
124+
.collect::<Vec<_>>();
125+
126+
if files.is_empty() {
127+
warn!("No files found. Are you in a git repository?");
128+
return Ok(());
129+
}
130+
131+
if let Ok(stderr) = String::from_utf8(git_output.stderr) {
132+
if !stderr.is_empty() {
133+
warn!("{}", &stderr);
134+
}
135+
}
136+
137+
let body = reqwest::Body::wrap_stream(streamreader);
138+
139+
request = request
140+
.body(body)
141+
.header(reqwest::header::CONTENT_TYPE, "application/x-tar");
142+
143+
(Some(tokio_tar::Builder::new(asyncwriter)), Some(files))
144+
}
145+
};
146+
147+
info!("Generating devenv.nix and devenv.yaml, this should take about a minute ...");
148+
149+
let response_future = request.send();
150+
151+
let tar_task = async {
152+
if let (Some(mut builder), Some(files)) = (body_sender, body) {
153+
for path in files {
154+
if path.is_file() && !cli.exclude.iter().any(|exclude| path.starts_with(exclude)) {
155+
builder.append_path(&path).await?;
156+
}
157+
}
158+
builder.finish().await?;
159+
}
160+
Ok::<(), std::io::Error>(())
161+
};
162+
163+
let (response, _) = tokio::join!(response_future, tar_task);
164+
165+
let response = response.into_diagnostic()?;
166+
let status = response.status();
167+
if !status.is_success() {
168+
let error_text = &response
169+
.text()
170+
.await
171+
.unwrap_or_else(|_| "No error details available".to_string());
172+
bail!(
173+
"Failed to generate (HTTP {}): {}",
174+
&status.as_u16(),
175+
match serde_json::from_str::<serde_json::Value>(error_text) {
176+
Ok(json) => json["message"]
177+
.as_str()
178+
.map(String::from)
179+
.unwrap_or_else(|| error_text.clone()),
180+
Err(_) => error_text.clone(),
181+
}
182+
);
183+
}
184+
185+
let response_json: GenerateResponse = response.json().await.expect("Failed to parse JSON.");
186+
187+
confirm_overwrite(Path::new("devenv.nix"), response_json.devenv_nix)?;
188+
confirm_overwrite(Path::new("devenv.yaml"), response_json.devenv_yaml)?;
189+
190+
info!(
191+
"{}",
192+
indoc::formatdoc!("
193+
Generated devenv.nix and devenv.yaml 🎉
194+
195+
Treat these as templates and open an issue at https://github.com/cachix/devenv/issues if you think we can do better!
196+
197+
Start by running:
198+
199+
$ devenv shell
200+
"));
201+
Ok(())
202+
}
203+
204+
fn confirm_overwrite(file: &Path, contents: String) -> Result<()> {
205+
if std::fs::metadata(file).is_ok() {
206+
// first output the old version and propose new changes
207+
let before = std::fs::read_to_string(file).expect("Failed to read file");
208+
209+
let diff = TextDiff::from_lines(&before, &contents);
210+
211+
println!("\nChanges that will be made to {}:", file.to_string_lossy());
212+
for change in diff.iter_all_changes() {
213+
let sign = match change.tag() {
214+
ChangeTag::Delete => "\x1b[31m-\x1b[0m",
215+
ChangeTag::Insert => "\x1b[32m+\x1b[0m",
216+
ChangeTag::Equal => " ",
217+
};
218+
print!("{}{}", sign, change);
219+
}
220+
221+
let confirm = dialoguer::Confirm::new()
222+
.with_prompt(format!(
223+
"{} already exists. Do you want to overwrite it?",
224+
file.to_string_lossy()
225+
))
226+
.interact()
227+
.into_diagnostic()?;
228+
229+
if confirm {
230+
std::fs::write(file, contents).into_diagnostic()?;
231+
}
232+
} else {
233+
std::fs::write(file, contents).into_diagnostic()?;
234+
}
235+
Ok(())
236+
}

devenv/Cargo.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ which.workspace = true
4545
whoami.workspace = true
4646
xdg.workspace = true
4747
tokio-tar.workspace = true
48-
tokio-util = { version = "0.7.12", features = ["io"] }
49-
similar = "2.6.0"
50-
binaryornot = "1.0.0"
51-
once_cell = "1.20.2"
48+
tokio-util.workspace = true
49+
similar.workspace = true
50+
binaryornot.workspace = true
51+
once_cell.workspace = true

devenv/src/cnix.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ impl<'a> Nix<'a> {
182182

183183
pub fn repl(&self) -> Result<()> {
184184
let mut cmd = self.prepare_command("nix", &["repl", "."], &self.options)?;
185-
cmd.exec();
185+
let _ = cmd.exec();
186186
Ok(())
187187
}
188188

@@ -425,7 +425,7 @@ impl<'a> Nix<'a> {
425425
&& cmd.get_program().to_string_lossy().ends_with("bin/nix")
426426
{
427427
info!("Starting Nix debugger ...");
428-
cmd.arg("--debugger").exec();
428+
let _ = cmd.arg("--debugger").exec();
429429
}
430430

431431
if options.bail_on_error {

0 commit comments

Comments
 (0)