Skip to content

Commit

Permalink
Move devenv generate to a separate binary
Browse files Browse the repository at this point in the history
  • Loading branch information
WeetHet committed Feb 18, 2025
1 parent 4c86a98 commit 6b618b3
Show file tree
Hide file tree
Showing 9 changed files with 316 additions and 149 deletions.
27 changes: 25 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
resolver = "2"
members = [
"devenv",
"devenv-generate",
"devenv-eval-cache",
"devenv-run-tests",
"devenv-tasks",
Expand All @@ -24,6 +25,7 @@ nix-conf-parser = { path = "nix-conf-parser" }
xtask = { path = "xtask" }

ansiterm = "0.12.2"
binaryornot = "1.0.0"
blake3 = "1.5.4"
clap = { version = "4.5.1", features = ["derive", "cargo", "env"] }
cli-table = "0.4.7"
Expand All @@ -38,6 +40,7 @@ indoc = "2.0.4"
lazy_static = "1.5.0"
miette = { version = "7.1.0", features = ["fancy"] }
nix = { version = "0.28.0", features = ["signal"] }
once_cell = "1.20.2"
petgraph = "0.6.5"
pretty_assertions = { version = "1.4.0", features = ["unstable"] }
regex = "1.10.3"
Expand All @@ -53,6 +56,7 @@ serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.114"
serde_repr = "0.1.19"
serde_yaml = "0.9.32"
similar = "2.6.0"
sha2 = "0.10.8"
sqlx = { version = "0.8.2", features = ["time", "sqlite", "runtime-tokio"] }
tempdir = "0.3.7"
Expand All @@ -70,6 +74,7 @@ tokio = { version = "1.39.3", features = [
"sync",
"time",
] }
tokio-util = { version = "0.7.12", features = ["io"] }
which = "6.0.0"
whoami = "1.5.1"
xdg = "2.5.2"
Expand Down
24 changes: 24 additions & 0 deletions devenv-generate/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
[package]
name = "devenv-generate"
version = "1.4.1"
edition.workspace = true
license.workspace = true

[dependencies]
devenv.workspace = true

clap = { workspace = true, features = ["derive"] }
console.workspace = true
dialoguer.workspace = true
indoc.workspace = true
miette.workspace = true
reqwest.workspace = true
serde.workspace = true
serde_json.workspace = true
tokio = { workspace = true, features = ["full"] }
tokio-tar.workspace = true
tracing.workspace = true
tokio-util.workspace = true
similar.workspace = true
binaryornot.workspace = true
once_cell.workspace = true
236 changes: 236 additions & 0 deletions devenv-generate/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
use clap::{crate_version, Parser};
use devenv::{
default_system,
log::{self, LogFormat},
};
use miette::{bail, IntoDiagnostic, Result};
use similar::{ChangeTag, TextDiff};
use std::path::{Path, PathBuf};
use tracing::{info, warn};

#[derive(Parser, Debug)]
#[command(
name = "devenv-generate",
about = "Generate devenv.yaml and devenv.nix using AI"
)]
struct Cli {
#[arg(num_args=0.., trailing_var_arg = true)]
description: Vec<String>,

#[clap(long, default_value = "https://devenv.new")]
host: String,

#[arg(
long,
help = "Paths to exclude during generation.",
value_name = "PATH"
)]
exclude: Vec<PathBuf>,

// https://consoledonottrack.com/
#[clap(long, env = "DO_NOT_TRACK", action = clap::ArgAction::SetTrue)]
disable_telemetry: bool,

#[arg(
short = 'V',
long,
global = true,
help = "Print version information",
long_help = "Print version information and exit"
)]
pub version: bool,

#[arg(short, long, global = true, default_value_t = default_system())]
pub system: String,

#[arg(short, long, global = true, help = "Enable additional debug logs.")]
verbose: bool,

#[arg(
short,
long,
global = true,
conflicts_with = "verbose",
help = "Silence all logs"
)]
pub quiet: bool,

#[arg(
long,
global = true,
help = "Configure the output format of the logs.",
default_value_t,
value_enum
)]
pub log_format: LogFormat,
}

#[derive(serde::Deserialize)]
struct GenerateResponse {
devenv_nix: String,
devenv_yaml: String,
}

#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();

if cli.version {
println!("devenv {} ({})", crate_version!(), cli.system);
return Ok(());
}

let level = if cli.verbose {
log::Level::Debug
} else if cli.quiet {
log::Level::Silent
} else {
log::Level::default()
};

log::init_tracing(level, cli.log_format);

let description = if !cli.description.is_empty() {
Some(cli.description.join(" "))
} else {
None
};

let client = reqwest::Client::new();
let mut request = client
.post(&cli.host)
.query(&[("disable_telemetry", cli.disable_telemetry)])
.header(reqwest::header::USER_AGENT, crate_version!());

let (asyncwriter, asyncreader) = tokio::io::duplex(256 * 1024);
let streamreader = tokio_util::io::ReaderStream::new(asyncreader);

let (body_sender, body) = match description {
Some(desc) => {
request = request.query(&[("q", desc)]);
(None, None)
}
None => {
let git_output = std::process::Command::new("git")
.args(["ls-files", "-z"])
.output()
.map_err(|_| miette::miette!("Failed to get list of files from git ls-files"))?;

let files = String::from_utf8_lossy(&git_output.stdout)
.split('\0')
.filter(|s| !s.is_empty())
.filter(|s| !binaryornot::is_binary(s).unwrap_or(false))
.map(PathBuf::from)
.collect::<Vec<_>>();

if files.is_empty() {
warn!("No files found. Are you in a git repository?");
return Ok(());
}

if let Ok(stderr) = String::from_utf8(git_output.stderr) {
if !stderr.is_empty() {
warn!("{}", &stderr);
}
}

let body = reqwest::Body::wrap_stream(streamreader);

request = request
.body(body)
.header(reqwest::header::CONTENT_TYPE, "application/x-tar");

(Some(tokio_tar::Builder::new(asyncwriter)), Some(files))
}
};

info!("Generating devenv.nix and devenv.yaml, this should take about a minute ...");

let response_future = request.send();

let tar_task = async {
if let (Some(mut builder), Some(files)) = (body_sender, body) {
for path in files {
if path.is_file() && !cli.exclude.iter().any(|exclude| path.starts_with(exclude)) {
builder.append_path(&path).await?;
}
}
builder.finish().await?;
}
Ok::<(), std::io::Error>(())
};

let (response, _) = tokio::join!(response_future, tar_task);

let response = response.into_diagnostic()?;
let status = response.status();
if !status.is_success() {
let error_text = &response
.text()
.await
.unwrap_or_else(|_| "No error details available".to_string());
bail!(
"Failed to generate (HTTP {}): {}",
&status.as_u16(),
match serde_json::from_str::<serde_json::Value>(error_text) {
Ok(json) => json["message"]
.as_str()
.map(String::from)
.unwrap_or_else(|| error_text.clone()),
Err(_) => error_text.clone(),
}
);
}

let response_json: GenerateResponse = response.json().await.expect("Failed to parse JSON.");

confirm_overwrite(Path::new("devenv.nix"), response_json.devenv_nix)?;
confirm_overwrite(Path::new("devenv.yaml"), response_json.devenv_yaml)?;

info!(
"{}",
indoc::formatdoc!("
Generated devenv.nix and devenv.yaml 🎉
Treat these as templates and open an issue at https://github.com/cachix/devenv/issues if you think we can do better!
Start by running:
$ devenv shell
"));
Ok(())
}

fn confirm_overwrite(file: &Path, contents: String) -> Result<()> {
if std::fs::metadata(file).is_ok() {
// first output the old version and propose new changes
let before = std::fs::read_to_string(file).expect("Failed to read file");

let diff = TextDiff::from_lines(&before, &contents);

println!("\nChanges that will be made to {}:", file.to_string_lossy());
for change in diff.iter_all_changes() {
let sign = match change.tag() {
ChangeTag::Delete => "\x1b[31m-\x1b[0m",
ChangeTag::Insert => "\x1b[32m+\x1b[0m",
ChangeTag::Equal => " ",
};
print!("{}{}", sign, change);
}

let confirm = dialoguer::Confirm::new()
.with_prompt(format!(
"{} already exists. Do you want to overwrite it?",
file.to_string_lossy()
))
.interact()
.into_diagnostic()?;

if confirm {
std::fs::write(file, contents).into_diagnostic()?;
}
} else {
std::fs::write(file, contents).into_diagnostic()?;
}
Ok(())
}
8 changes: 4 additions & 4 deletions devenv/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ which.workspace = true
whoami.workspace = true
xdg.workspace = true
tokio-tar.workspace = true
tokio-util = { version = "0.7.12", features = ["io"] }
similar = "2.6.0"
binaryornot = "1.0.0"
once_cell = "1.20.2"
tokio-util.workspace = true
similar.workspace = true
binaryornot.workspace = true
once_cell.workspace = true
4 changes: 2 additions & 2 deletions devenv/src/cnix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ impl<'a> Nix<'a> {

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

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

if options.bail_on_error {
Expand Down
Loading

0 comments on commit 6b618b3

Please sign in to comment.