nix_data/cache/
nixos.rs

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
15/// Downloads the latest `packages.json` for the system from the NixOS cache and returns the path to an SQLite database `nixospkgs.db` which contains package data.
16/// Will only work on NixOS systems.
17pub 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 cache directory doesn't exist, create it
22    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        // Internet connection failed
36        // Check if we can use the old database
37        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    // Check if latest version is already downloaded
63    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, // buffer size
86            );
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        // Write version downloaded to file
109        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
117/// Downloads the latest 'options.json' for the system from the NixOS cache and returns the path to the file.
118/// Will only work on NixOS systems.
119pub 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 cache directory doesn't exist, create it
124    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    // Download file with reqwest blocking
160    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        // Write version downloaded to file
166        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}