nix_data/cache/
profile.rs

1use crate::{
2    cache::nixos,
3    CACHEDIR,
4};
5use anyhow::{anyhow, Context, Result};
6use log::{debug, error, info};
7use serde::Deserialize;
8use sqlx::SqlitePool;
9use std::{
10    collections::{HashMap, HashSet},
11    fs::{self, File},
12    io::{Read, Write},
13    path::Path,
14    process::Command,
15};
16
17use super::nixos::nixospkgs;
18
19#[derive(Debug, Deserialize)]
20struct ProfilePkgsRoot {
21    elements: Vec<ProfilePkgOut>,
22}
23
24#[derive(Debug, Deserialize)]
25struct ProfilePkgOut {
26    #[serde(rename = "attrPath")]
27    attrpath: Option<String>,
28    #[serde(rename = "originalUrl")]
29    originalurl: Option<String>,
30    #[serde(rename = "storePaths")]
31    storepaths: Vec<String>,
32}
33
34/// Struct containing information about a package installed with `nix profile`.
35#[derive(Debug)]
36pub struct ProfilePkg {
37    pub name: String,
38    pub originalurl: String,
39}
40
41/// Returns a list of all packages installed with `nix profile` with their name.
42/// Does not include individual version.
43pub fn getprofilepkgs() -> Result<HashMap<String, ProfilePkg>> {
44    if !Path::new(&format!(
45        "{}/.nix-profile/manifest.json",
46        std::env::var("HOME")?
47    ))
48    .exists()
49    {
50        return Ok(HashMap::new());
51    }
52    let profileroot: ProfilePkgsRoot = serde_json::from_reader(File::open(&format!(
53        "{}/.nix-profile/manifest.json",
54        std::env::var("HOME")?
55    ))?)?;
56    let mut out = HashMap::new();
57    for pkg in profileroot.elements {
58        if let (Some(attrpath), Some(originalurl)) = (pkg.attrpath, pkg.originalurl) {
59            let attr = if attrpath.starts_with("legacyPackages") {
60                attrpath
61                    .split('.')
62                    .collect::<Vec<_>>()
63                    .get(2..)
64                    .context("Failed to get legacyPackage attribute")?
65                    .join(".")
66            } else {
67                format!("{}#{}", originalurl, attrpath)
68            };
69            if let Some(first) = pkg.storepaths.get(0) {
70                let ver = first
71                    .get(44..)
72                    .context("Failed to get pkg name from store path")?;
73                out.insert(
74                    attr,
75                    ProfilePkg {
76                        name: ver.to_string(),
77                        originalurl,
78                    },
79                );
80            }
81        }
82    }
83    Ok(out)
84}
85
86/// Returns a list of all packages installed with `nix profile` with their name and version.
87/// Takes a bit longer than [getprofilepkgs()].
88pub async fn getprofilepkgs_versioned() -> Result<HashMap<String, String>> {
89    if !Path::new(&format!(
90        "{}/.nix-profile/manifest.json",
91        std::env::var("HOME")?
92    ))
93    .exists()
94    {
95        return Ok(HashMap::new());
96    }
97    let profilepkgs = getprofilepkgs()?;
98    let latestpkgs = if Path::new(&format!("{}/nixpkgs.db", &*CACHEDIR)).exists() {
99        format!("{}/nixpkgs.db", &*CACHEDIR)
100    } else {
101        // Change to something else if overridden
102        nixpkgslatest().await?
103    };
104    let mut out = HashMap::new();
105    let pool = SqlitePool::connect(&format!("sqlite://{}", latestpkgs)).await?;
106    for (pkg, _v) in profilepkgs {
107        let versions: Vec<(String,)> = sqlx::query_as(
108            r#"
109            SELECT version FROM pkgs WHERE attribute = $1
110            "#,
111        )
112        .bind(&pkg)
113        .fetch_all(&pool)
114        .await?;
115        if !versions.is_empty() {
116            out.insert(pkg, versions.get(0).unwrap().0.to_string());
117        }
118    }
119    Ok(out)
120}
121
122/// Downloads a list of available package versions `packages.db`
123/// and returns the path to the file.
124pub async fn nixpkgslatest() -> Result<String> {
125    // If cache directory doesn't exist, create it
126    if !std::path::Path::new(&*CACHEDIR).exists() {
127        std::fs::create_dir_all(&*CACHEDIR)?;
128    }
129
130    let mut nixpkgsver = None;
131    let mut pinned = false;
132    let regout = Command::new("nix").arg("registry").arg("list").output()?;
133    let reg = String::from_utf8(regout.stdout)?.replace("   ", " ");
134
135    let mut latestnixpkgsver = String::new();
136
137    for l in reg.split('\n') {
138        let parts = l.split(' ').collect::<Vec<_>>();
139        if let Some(x) = parts.get(1) {
140            if x == &"flake:nixpkgs" {
141                if let Some(x) = parts.get(2) {
142                    nixpkgsver = Some(x.to_string().replace("github:NixOS/nixpkgs/", ""));
143
144                    if let Some(rev) = x.find("&rev=") {
145                        if let Some(rev) = (*x).get(rev + 5..) {
146                            info!(
147                                "Found specific revision: {}. Switching to versioned checking",
148                                rev
149                            );
150                            nixpkgsver = Some(rev.to_string());
151                            latestnixpkgsver = rev.to_string();
152                            pinned = true;
153                        }
154                    }
155                    break;
156                }
157            }
158        }
159    }
160
161    if !pinned {
162        let verurl = if let Some(v) = &nixpkgsver {
163            format!(
164                "https://raw.githubusercontent.com/snowflakelinux/nix-data-db/main/{}/nixpkgs.ver",
165                v
166            )
167        } else {
168            String::from("https://raw.githubusercontent.com/snowflakelinux/nix-data-db/main/nixpkgs-unstable/nixpkgs.ver")
169        };
170        debug!("Checking nixpkgs version");
171        let resp = reqwest::get(&verurl).await;
172        let resp = if let Ok(r) = resp {
173            r
174        } else {
175            // Internet connection failed
176            // Check if we can use the old database
177            let dbpath = format!("{}/nixpkgs.db", &*CACHEDIR);
178            if Path::new(&dbpath).exists() {
179                info!("Using old database");
180                return Ok(dbpath);
181            } else {
182                return Err(anyhow!("Could not find latest nixpkgs version"));
183            }
184        };
185        latestnixpkgsver = if resp.status().is_success() {
186            resp.text().await?
187        } else {
188            return Err(anyhow!("Could not find latest nixpkgs version"));
189        };
190        debug!("Latest nixpkgs version: {}", latestnixpkgsver);
191    }
192
193    // Check if latest version is already downloaded
194    if let Ok(prevver) = fs::read_to_string(&format!("{}/nixpkgs.ver", &*CACHEDIR)) {
195        if prevver == latestnixpkgsver && Path::new(&format!("{}/nixpkgs.db", &*CACHEDIR)).exists()
196        {
197            debug!("No new version of nixpkgs found");
198            return Ok(format!("{}/nixpkgs.db", &*CACHEDIR));
199        }
200    }
201
202    let url = if pinned {
203        format!(
204            "https://raw.githubusercontent.com/snowflakelinux/nixpkgs-version-data/main/nixos-unstable/{}.json.br",
205            latestnixpkgsver
206        )
207    } else if let Some(v) = &nixpkgsver {
208        format!(
209            "https://raw.githubusercontent.com/snowflakelinux/nix-data-db/main/{}/nixpkgs_versions.db.br",
210            v
211        )
212    } else {
213        String::from("https://raw.githubusercontent.com/snowflakelinux/nix-data-db/main/nixpkgs-unstable/nixpkgs_versions.db.br")
214    };
215    debug!("Downloading nix-data database");
216    let client = reqwest::Client::builder().brotli(true).build()?;
217    let resp = client.get(url).send().await?;
218    if resp.status().is_success() {
219        debug!("Writing nix-data database");
220        // let mut out = File::create(&format!("{}/nixpkgs.db", &*CACHEDIR))?;
221        {
222            let bytes = resp.bytes().await?;
223            let mut br = brotli::Decompressor::new(bytes.as_ref(), 4096);
224            let mut pkgsout = Vec::new();
225            br.read_to_end(&mut pkgsout)?;
226            if pinned {
227                let pkgsjson: HashMap<String, String> = serde_json::from_slice(&pkgsout)?;
228                let dbfile = format!("{}/nixpkgs.db", &*CACHEDIR);
229                nixos::createdb(&dbfile, &pkgsjson).await?;
230            } else {
231                let mut out = File::create(&format!("{}/nixpkgs.db", &*CACHEDIR))?;
232                if let Err(e) = out.write_all(&pkgsout) {
233                    error!("{}", e);
234                    return Err(anyhow!("Failed write to nixpkgs.br"));
235                }
236            }
237        }
238        debug!("Writing nix-data version");
239        // Write version downloaded to file
240        File::create(format!("{}/nixpkgs.ver", &*CACHEDIR))?
241            .write_all(latestnixpkgsver.as_bytes())?;
242    } else {
243        return Err(anyhow!("Failed to download latest nixpkgs.db.br"));
244    }
245    Ok(format!("{}/nixpkgs.db", &*CACHEDIR))
246}
247
248pub async fn unavailablepkgs() -> Result<HashMap<String, String>> {
249    let nixpath = Command::new("nix")
250        .arg("eval")
251        .arg("nixpkgs#path")
252        .output()?
253        .stdout;
254    let nixpath = String::from_utf8(nixpath)?;
255    let nixpath = nixpath.trim();
256
257    let aliases = Command::new("nix-instantiate")
258        .arg("--eval")
259        .arg("-E")
260        .arg(&format!("with import {} {{}}; builtins.attrNames ((self: super: lib.optionalAttrs config.allowAliases (import {}/pkgs/top-level/aliases.nix lib self super)) {{}} {{}})", nixpath, nixpath))
261        .arg("--json")
262        .output()?;
263    let aliasstr = String::from_utf8(aliases.stdout)?;
264    let aliasesout: HashSet<String> = serde_json::from_str(&aliasstr)?;
265
266    let flakespkgs = getprofilepkgs()?;
267    let mut unavailable = HashMap::new();
268    for pkg in flakespkgs.keys() {
269        if aliasesout.contains(pkg) && Command::new("nix-instantiate")
270                .arg("--eval")
271                .arg("-E")
272                .arg(&format!("with import {} {{}}; builtins.tryEval ((self: super: lib.optionalAttrs config.allowAliases (import {}/pkgs/top-level/aliases.nix lib self super)) {{}} {{}}).{}", nixpath, nixpath, pkg))
273                .output()?.status.success() {
274            let out = Command::new("nix-instantiate")
275                .arg("--eval")
276                .arg("-E")
277                .arg(&format!("with import {} {{}}; ((self: super: lib.optionalAttrs config.allowAliases (import {}/pkgs/top-level/aliases.nix lib self super)) {{}} {{}}).{}", nixpath, nixpath, pkg))
278                .output()?;
279            let err = String::from_utf8(out.stderr)?;
280            let err = err.strip_prefix("error: ").unwrap_or(&err).trim();
281            unavailable.insert(pkg.to_string(), err.to_string());
282        }
283    }
284
285    let nixospkgs = nixospkgs().await?;
286    let pool = SqlitePool::connect(&format!("sqlite://{}", nixospkgs)).await?;
287
288    for pkg in flakespkgs.keys() {
289        let (x, broken, insecure): (String, u8, u8) =
290            sqlx::query_as("SELECT attribute,broken,insecure FROM meta WHERE attribute = $1")
291                .bind(pkg)
292                .fetch_one(&pool)
293                .await?;
294        if &x != pkg {
295            unavailable.insert(
296                pkg.to_string(),
297                String::from("Package not found in newer version of nixpkgs"),
298            );
299        } else if broken == 1 {
300            unavailable.insert(pkg.to_string(), String::from("Package is marked as broken"));
301        } else if insecure == 1 {
302            unavailable.insert(
303                pkg.to_string(),
304                String::from("Package is marked as insecure"),
305            );
306        }
307    }
308    Ok(unavailable)
309}