nix_data/cache/
flakes.rs

1use crate::CACHEDIR;
2use anyhow::{Context, Result};
3use log::info;
4use sqlx::SqlitePool;
5use std::{
6    collections::{HashMap, HashSet},
7    fs::{self, File},
8    io::{Read, Write},
9    path::Path,
10    process::Command,
11};
12
13use super::{
14    nixos::{self, getnixospkgs, nixospkgs},
15    NixPkg,
16};
17
18/// Gets a list of all packages in the NixOS system with their name and version.
19/// Can be used to find what versions of system packages are currently installed.
20/// Will only work on NixOS systems.
21pub async fn flakespkgs() -> Result<String> {
22    let versionout = Command::new("nixos-version").arg("--json").output()?;
23    let version: HashMap<String, String> = serde_json::from_slice(&versionout.stdout)?;
24
25    let nixosversion = version
26        .get("nixosVersion")
27        .context("No NixOS version found")?;
28
29    // If cache directory doesn't exist, create it
30    if !std::path::Path::new(&*CACHEDIR).exists() {
31        std::fs::create_dir_all(&*CACHEDIR)?;
32    }
33
34    // Check if latest version is already downloaded
35    if let Ok(prevver) = fs::read_to_string(&format!("{}/flakespkgs.ver", &*CACHEDIR)) {
36        if prevver.eq(nixosversion) && Path::new(&format!("{}/flakespkgs.db", &*CACHEDIR)).exists()
37        {
38            info!("No new version of NixOS flakes found");
39            return Ok(format!("{}/flakespkgs.db", &*CACHEDIR));
40        }
41    }
42
43    // Get list of packages from flake
44    let pkgsout = if let Some(rev) = version.get("nixpkgsRevision") {
45        let url = format!("https://raw.githubusercontent.com/snowflakelinux/nixpkgs-version-data/main/nixos-{}/{}.json.br", nixosversion.get(0..5).context("Invalid NixOS version")?, rev);
46        let resp = reqwest::get(&url).await?;
47        if resp.status().is_success() {
48            let r = resp.bytes().await?;
49            let mut br = brotli::Decompressor::new(r.as_ref(), 4096);
50            let mut pkgsout = Vec::new();
51            br.read_to_end(&mut pkgsout)?;
52            let pkgsjson: HashMap<String, String> = serde_json::from_slice(&pkgsout)?;
53            pkgsjson
54        } else {
55            let url = format!("https://raw.githubusercontent.com/snowflakelinux/nixpkgs-version-data/main/nixos-unstable/{}.json.br", rev);
56            let resp = reqwest::get(&url).await?;
57            if resp.status().is_success() {
58                let r = resp.bytes().await?;
59                let mut br = brotli::Decompressor::new(r.as_ref(), 4096);
60                let mut pkgsout = Vec::new();
61                br.read_to_end(&mut pkgsout)?;
62                let pkgsjson: HashMap<String, String> = serde_json::from_slice(&pkgsout)?;
63                pkgsjson
64            } else {
65                let pkgsout = Command::new("nix")
66                    .arg("search")
67                    .arg("--json")
68                    .arg(&format!("nixpkgs/{}", rev))
69                    .output()?;
70                let pkgsjson: HashMap<String, NixPkg> =
71                    serde_json::from_str(&String::from_utf8(pkgsout.stdout)?)?;
72                let pkgsjson = pkgsjson
73                    .iter()
74                    .map(|(k, v)| {
75                        (
76                            k.split('.').collect::<Vec<_>>()[2..].join("."),
77                            v.version.to_string(),
78                        )
79                    })
80                    .collect::<HashMap<String, String>>();
81                pkgsjson
82            }
83        }
84    } else {
85        let pkgsout = Command::new("nix")
86            .arg("search")
87            .arg("--json")
88            // .arg("--inputs-from")
89            // .arg(&flakepath)
90            .arg("nixpkgs")
91            .output()?;
92        let pkgsjson: HashMap<String, NixPkg> =
93            serde_json::from_str(&String::from_utf8(pkgsout.stdout)?)?;
94        let pkgsjson = pkgsjson
95            .iter()
96            .map(|(k, v)| {
97                (
98                    k.split('.').collect::<Vec<_>>()[2..].join("."),
99                    v.version.to_string(),
100                )
101            })
102            .collect::<HashMap<String, String>>();
103        pkgsjson
104    };
105
106    let dbfile = format!("{}/flakespkgs.db", &*CACHEDIR);
107    nixos::createdb(&dbfile, &pkgsout).await?;
108
109    // Write version downloaded to file
110    File::create(format!("{}/flakespkgs.ver", &*CACHEDIR))?.write_all(nixosversion.as_bytes())?;
111
112    Ok(format!("{}/flakespkgs.db", &*CACHEDIR))
113}
114
115/// Returns a list of all installed system packages with their attribute and version
116/// The input `paths` should be the paths to the `configuration.nix` files containing `environment.systemPackages`
117pub async fn getflakepkgs(paths: &[&str]) -> Result<HashMap<String, String>> {
118    getnixospkgs(paths, nixos::NixosType::Flake).await
119}
120
121pub fn uptodate() -> Result<Option<(String, String)>> {
122    let flakesver = fs::read_to_string(&format!("{}/flakespkgs.ver", &*CACHEDIR))?;
123    let nixosver = fs::read_to_string(&format!("{}/nixospkgs.ver", &*CACHEDIR))?;
124    let flakeslast = flakesver
125        .split('.')
126        .collect::<Vec<_>>()
127        .last()
128        .context("Invalid version")?
129        .to_string();
130    let nixoslast = nixosver
131        .split('.')
132        .collect::<Vec<_>>()
133        .last()
134        .context("Invalid version")?
135        .to_string();
136    if !nixoslast.starts_with(&flakeslast) {
137        Ok(Some((flakesver, nixosver)))
138    } else {
139        Ok(None)
140    }
141}
142
143pub async fn unavailablepkgs(paths: &[&str]) -> Result<HashMap<String, String>> {
144    let versionout = Command::new("nixos-version").arg("--json").output()?;
145    let version: HashMap<String, String> = serde_json::from_slice(&versionout.stdout)?;
146    let nixpath = if let Some(rev) = version.get("nixpkgsRevision") {
147        Command::new("nix")
148            .arg("eval")
149            .arg(&format!("nixpkgs/{}#path", rev))
150            .output()?
151            .stdout
152    } else {
153        Command::new("nix")
154            .arg("eval")
155            .arg("nixpkgs#path")
156            .output()?
157            .stdout
158    };
159    let nixpath = String::from_utf8(nixpath)?;
160    let nixpath = nixpath.trim();
161
162    let aliases = Command::new("nix-instantiate")
163        .arg("--eval")
164        .arg("-E")
165        .arg(&format!("with import {} {{}}; builtins.attrNames ((self: super: lib.optionalAttrs config.allowAliases (import {}/pkgs/top-level/aliases.nix lib self super)) {{}} {{}})", nixpath, nixpath))
166        .arg("--json")
167        .output()?;
168    let aliasstr = String::from_utf8(aliases.stdout)?;
169    let aliasesout: HashSet<String> = serde_json::from_str(&aliasstr)?;
170
171    let pkgs = {
172        let mut allpkgs: HashSet<String> = HashSet::new();
173        for path in paths {
174            if let Ok(filepkgs) = nix_editor::read::getarrvals(
175                &fs::read_to_string(path)?,
176                "environment.systemPackages",
177            ) {
178                let filepkgset = filepkgs
179                    .into_iter()
180                    .map(|x| x.strip_prefix("pkgs.").unwrap_or(&x).to_string())
181                    .collect::<HashSet<_>>();
182                allpkgs = allpkgs.union(&filepkgset).map(|x| x.to_string()).collect();
183            }
184        }
185        allpkgs
186    };
187
188    let mut unavailable = HashMap::new();
189    for pkg in pkgs {
190        if aliasesout.contains(&pkg) && Command::new("nix-instantiate")
191                .arg("--eval")
192                .arg("-E")
193                .arg(&format!("with import {} {{}}; builtins.tryEval ((self: super: lib.optionalAttrs config.allowAliases (import {}/pkgs/top-level/aliases.nix lib self super)) {{}} {{}}).{}", nixpath, nixpath, pkg))
194                .output()?.status.success() {
195            let out = Command::new("nix-instantiate")
196                .arg("--eval")
197                .arg("-E")
198                .arg(&format!("with import {} {{}}; ((self: super: lib.optionalAttrs config.allowAliases (import {}/pkgs/top-level/aliases.nix lib self super)) {{}} {{}}).{}", nixpath, nixpath, pkg))
199                .output()?;
200            let err = String::from_utf8(out.stderr)?;
201            let err = err.strip_prefix("error: ").unwrap_or(&err).trim();
202            unavailable.insert(pkg, err.to_string());
203        }
204    }
205
206    let profilepkgs = getflakepkgs(paths).await?;
207    let nixospkgs = nixospkgs().await?;
208    let pool = SqlitePool::connect(&format!("sqlite://{}", nixospkgs)).await?;
209
210    for (pkg, _) in profilepkgs {
211        let (x, broken, insecure): (String, u8, u8) =
212            sqlx::query_as("SELECT attribute,broken,insecure FROM meta WHERE attribute = $1")
213                .bind(&pkg)
214                .fetch_one(&pool)
215                .await?;
216        if x != pkg {
217            unavailable.insert(
218                pkg,
219                String::from("Package not found in newer version of nixpkgs"),
220            );
221        } else if broken == 1 {
222            unavailable.insert(pkg, String::from("Package is marked as broken"));
223        } else if insecure == 1 {
224            unavailable.insert(pkg, String::from("Package is marked as insecure"));
225        }
226    }
227    Ok(unavailable)
228}