1use crate::CACHEDIR;
2use anyhow::{anyhow, Context, Result};
3use log::{debug, info};
4use sqlx::{migrate::MigrateDatabase, Row, Sqlite, SqlitePool};
5use std::{
6 collections::{HashMap, HashSet},
7 fs::{self, File},
8 io::{Read, Write},
9 path::Path,
10 process::{Command, Stdio},
11};
12
13use super::{channel, flakes};
14
15pub async fn nixospkgs() -> Result<String> {
18 let versionout = Command::new("nixos-version").output()?;
19 let mut version = &String::from_utf8(versionout.stdout)?[0..5];
20
21 if !std::path::Path::new(&*CACHEDIR).exists() {
23 std::fs::create_dir_all(&*CACHEDIR)?;
24 }
25
26 let verurl = format!(
27 "https://raw.githubusercontent.com/snowflakelinux/nix-data-db/main/nixos-{}/nixpkgs.ver",
28 version
29 );
30 debug!("Checking NixOS version");
31 let resp = reqwest::get(&verurl);
32 let resp = if let Ok(r) = resp.await {
33 r
34 } else {
35 let dbpath = format!("{}/nixospkgs.db", &*CACHEDIR);
38 if Path::new(&dbpath).exists() {
39 info!("Using old database");
40 return Ok(dbpath);
41 } else {
42 return Err(anyhow!("Could not find latest NixOS version"));
43 }
44 };
45 let latestnixosver = if resp.status().is_success() {
46 resp.text().await?
47 } else {
48 let resp = reqwest::get("https://raw.githubusercontent.com/snowflakelinux/nix-data-db/main/nixos-unstable/nixpkgs.ver").await?;
49 if resp.status().is_success() {
50 version = "unstable";
51 resp.text().await?
52 } else {
53 return Err(anyhow!("Could not find latest NixOS version"));
54 }
55 };
56 debug!("Latest NixOS version: {}", latestnixosver);
57
58 let latestnixosver = latestnixosver
59 .strip_prefix("nixos-")
60 .unwrap_or(&latestnixosver);
61 info!("latestnixosver: {}", latestnixosver);
62 if let Ok(prevver) = fs::read_to_string(&format!("{}/nixospkgs.ver", &*CACHEDIR)) {
64 if prevver == latestnixosver && Path::new(&format!("{}/nixospkgs.db", &*CACHEDIR)).exists()
65 {
66 debug!("No new version of NixOS found");
67 return Ok(format!("{}/nixospkgs.db", &*CACHEDIR));
68 }
69 }
70
71 let url = format!(
72 "https://raw.githubusercontent.com/snowflakelinux/nix-data-db/main/nixos-{}/nixpkgs.db.br",
73 version
74 );
75 debug!("Downloading nix-data database");
76 let client = reqwest::Client::builder().brotli(true).build()?;
77 let resp = client.get(url).send().await?;
78 if resp.status().is_success() {
79 debug!("Writing nix-data database");
80 let mut out = File::create(&format!("{}/nixospkgs.db", &*CACHEDIR))?;
81 {
82 let bytes = resp.bytes().await?;
83 let mut reader = brotli::Decompressor::new(
84 bytes.as_ref(),
85 4096, );
87 let mut buf = [0u8; 4096];
88 loop {
89 match reader.read(&mut buf[..]) {
90 Err(e) => {
91 if let std::io::ErrorKind::Interrupted = e.kind() {
92 continue;
93 }
94 panic!("{}", e);
95 }
96 Ok(size) => {
97 if size == 0 {
98 break;
99 }
100 if let Err(e) = out.write_all(&buf[..size]) {
101 panic!("{}", e)
102 }
103 }
104 }
105 }
106 }
107 debug!("Writing nix-data version");
108 File::create(format!("{}/nixospkgs.ver", &*CACHEDIR))?
110 .write_all(latestnixosver.as_bytes())?;
111 } else {
112 return Err(anyhow!("Failed to download latest nixospkgs.db.br"));
113 }
114 Ok(format!("{}/nixospkgs.db", &*CACHEDIR))
115}
116
117pub fn nixosoptions() -> Result<String> {
120 let versionout = Command::new("nixos-version").output()?;
121 let mut version = &String::from_utf8(versionout.stdout)?[0..5];
122
123 if !std::path::Path::new(&*CACHEDIR).exists() {
125 std::fs::create_dir_all(&*CACHEDIR)?;
126 }
127
128 let verurl = format!("https://channels.nixos.org/nixos-{}", version);
129 debug!("Checking NixOS version");
130 let resp = reqwest::blocking::get(&verurl)?;
131 let latestnixosver = if resp.status().is_success() {
132 resp.url()
133 .path_segments()
134 .context("No path segments found")?
135 .last()
136 .context("Last element not found")?
137 .to_string()
138 } else {
139 let resp = reqwest::blocking::get("https://channels.nixos.org/nixos-unstable")?;
140 if resp.status().is_success() {
141 version = "unstable";
142 resp.url()
143 .path_segments()
144 .context("No path segments found")?
145 .last()
146 .context("Last element not found")?
147 .to_string()
148 } else {
149 return Err(anyhow!("Could not find latest NixOS version"));
150 }
151 };
152 debug!("Latest NixOS version: {}", latestnixosver);
153
154 let url = format!(
155 "https://channels.nixos.org/nixos-{}/options.json.br",
156 version
157 );
158
159 let client = reqwest::blocking::Client::builder().brotli(true).build()?;
161 let mut resp = client.get(url).send()?;
162 if resp.status().is_success() {
163 let mut out = File::create(&format!("{}/nixosoptions.json", &*CACHEDIR))?;
164 resp.copy_to(&mut out)?;
165 File::create(format!("{}/nixosoptions.ver", &*CACHEDIR))?
167 .write_all(latestnixosver.as_bytes())?;
168 } else {
169 return Err(anyhow!("Failed to download latest options.json"));
170 }
171
172 Ok(format!("{}/nixosoptions.json", &*CACHEDIR))
173}
174
175pub(super) enum NixosType {
176 Flake,
177 Legacy,
178}
179
180pub(super) async fn getnixospkgs(
181 paths: &[&str],
182 nixos: NixosType,
183) -> Result<HashMap<String, String>> {
184 let pkgs = {
185 let mut allpkgs: HashSet<String> = HashSet::new();
186 for path in paths {
187 if let Ok(filepkgs) = nix_editor::read::getarrvals(
188 &fs::read_to_string(path)?,
189 "environment.systemPackages",
190 ) {
191 let filepkgset = filepkgs
192 .into_iter()
193 .map(|x| x.strip_prefix("pkgs.").unwrap_or(&x).to_string())
194 .collect::<HashSet<_>>();
195 allpkgs = allpkgs.union(&filepkgset).map(|x| x.to_string()).collect();
196 }
197 }
198 allpkgs
199 };
200 debug!("getnixospkgs: {:?}", pkgs);
201 let pkgsdb = match nixos {
202 NixosType::Flake => flakes::flakespkgs().await?,
203 NixosType::Legacy => channel::legacypkgs().await?,
204 };
205 let mut out = HashMap::new();
206 let pool = SqlitePool::connect(&format!("sqlite://{}", pkgsdb)).await?;
207 for pkg in pkgs {
208 let mut sqlout = sqlx::query(
209 r#"
210 SELECT version FROM pkgs WHERE attribute = $1
211 "#,
212 )
213 .bind(&pkg)
214 .fetch_all(&pool)
215 .await?;
216 if sqlout.len() == 1 {
217 let row = sqlout.pop().unwrap();
218 let version: String = row.get("version");
219 out.insert(pkg, version);
220 }
221 }
222 Ok(out)
223}
224
225pub(super) async fn createdb(dbfile: &str, pkgjson: &HashMap<String, String>) -> Result<()> {
226 let db = format!("sqlite://{}", dbfile);
227 if Path::new(dbfile).exists() {
228 fs::remove_file(dbfile)?;
229 }
230 Sqlite::create_database(&db).await?;
231 let pool = SqlitePool::connect(&db).await?;
232 sqlx::query(
233 r#"
234 CREATE TABLE "pkgs" (
235 "attribute" TEXT NOT NULL UNIQUE,
236 "version" TEXT,
237 PRIMARY KEY("attribute")
238 )
239 "#,
240 )
241 .execute(&pool)
242 .await?;
243 sqlx::query(
244 r#"
245 CREATE UNIQUE INDEX "attributes" ON "pkgs" ("attribute")
246 "#,
247 )
248 .execute(&pool)
249 .await?;
250
251 let mut wtr = csv::Writer::from_writer(vec![]);
252 for (pkg, version) in pkgjson {
253 wtr.serialize((pkg.to_string(), version.to_string()))?;
254 }
255 let data = String::from_utf8(wtr.into_inner()?)?;
256 let mut cmd = Command::new("sqlite3")
257 .arg("-csv")
258 .arg(dbfile)
259 .arg(".import '|cat -' pkgs")
260 .stdin(Stdio::piped())
261 .spawn()?;
262 let cmd_stdin = cmd.stdin.as_mut().unwrap();
263 cmd_stdin.write_all(data.as_bytes())?;
264 let _status = cmd.wait()?;
265 Ok(())
266}