nix_build/
config.rs

1use std::{
2    collections::HashMap,
3    ffi::{OsStr, OsString},
4    path::{Path, PathBuf},
5    process::Command,
6};
7
8use crate::{Error, Result};
9
10enum NixTarget {
11    Function(OsString),
12    Flake(String),
13    Expr(String),
14}
15
16impl Default for NixTarget {
17    fn default() -> Self {
18        Self::Function(OsString::from("default.nix"))
19    }
20}
21
22/// Build style configration for a pending build.
23pub struct Config {
24    target: NixTarget,
25    arg_exprs: Vec<(String, String)>,
26    arg_strs: Vec<(String, String)>,
27    impure: bool,
28}
29
30impl Default for Config {
31    fn default() -> Self {
32        Self::new()
33    }
34}
35
36/// Represents a nix build output derivation
37#[derive(Debug, serde::Deserialize)]
38pub struct Derivation {
39    #[serde(alias = "drvPath")]
40    /// Derivation path
41    pub drv_path: PathBuf,
42    /// List of outputs for this derivation
43    ///
44    /// Example outputs: `out`, `dev`
45    pub outputs: HashMap<String, PathBuf>,
46}
47
48impl Derivation {
49    pub fn out(&self) -> Option<&PathBuf> {
50        self.outputs.get("out")
51    }
52}
53
54impl Config {
55    /// Create a new nix build [`Config`]
56    ///
57    /// Target is defaulted to `default.nix`
58    pub fn new() -> Self {
59        Self {
60            target: NixTarget::default(),
61            arg_exprs: vec![],
62            arg_strs: vec![],
63            impure: false,
64        }
65    }
66
67    /// Add an expression argument to the invoked nix expression
68    ///
69    /// # Example
70    /// ```
71    /// Config::default().arg_expr("{pkgs}:pkgs.hello").arg_expr("pkgs", "import <nixpkgs> {}")
72    /// ```
73    pub fn arg_expr(&mut self, name: &str, value: &str) -> &mut Self {
74        self.arg_exprs.push((name.to_owned(), value.to_owned()));
75        self
76    }
77
78    /// Add a string argument to the invoked nix expression
79    ///
80    /// # Example
81    /// ```
82    /// Config::default().arg_str("{pkgs,name}:pkgs.hello.overrideAttrs (_: {inherit name;})").arg_str("name", "not-hello")
83    /// ```
84    pub fn arg_str(&mut self, name: &str, value: &str) -> &mut Self {
85        self.arg_strs.push((name.to_owned(), value.to_owned()));
86        self
87    }
88
89    /// Build the derivation described by the given .nix file
90    ///
91    /// # Example
92    /// ```
93    /// Config::default().target_file("hello.nix")
94    /// ```
95    pub fn target_file(&mut self, filename: impl AsRef<OsStr>) -> &mut Self {
96        self.target = NixTarget::Function(filename.as_ref().to_owned());
97        self
98    }
99
100    /// Build the derivation described by the given flake output
101    ///
102    /// # Example
103    /// ```
104    /// Config::default().target_flake("nixpkgs#hello")
105    /// ```
106    pub fn target_flake(&mut self, flake: &str) -> &mut Self {
107        self.target = NixTarget::Flake(flake.to_owned());
108        self
109    }
110
111    /// Build the derivation described by the given expression
112    ///
113    /// # Example
114    /// ```
115    /// Config::default().target_expr("{pkgs}: pkgs.hello").build()
116    /// ```
117    pub fn target_expr(&mut self, expr: &str) -> &mut Self {
118        self.target = NixTarget::Expr(expr.to_owned());
119        self
120    }
121
122    /// Set to enable impure evaluation mode
123    ///
124    /// Will pass the `--impure` flag to the invocation if set
125    pub fn impure(&mut self, impure: bool) -> &mut Self {
126        self.impure = impure;
127        self
128    }
129
130    /// Invoke `nix build` with the given configuration
131    #[must_use]
132    pub fn build(&self) -> Result<Vec<Derivation>> {
133        let nix = crate::is_nix_available().ok_or(Error::NixNotAvailable)?;
134
135        let cwd = std::env::current_dir().unwrap();
136        let mut cmd = Command::new(nix);
137        cmd.current_dir(&cwd);
138        cmd.arg("build");
139
140        cmd.args(&["--no-link", "--json"]);
141
142        match &self.target {
143            NixTarget::Function(file) => {
144                cmd.args(&[OsStr::new("-f"), &file]);
145
146                // make sure the build script is rerun if the file changes
147                println!(
148                    "cargo:rerun-if-changed={}",
149                    AsRef::<Path>::as_ref(file).display()
150                );
151            }
152            NixTarget::Flake(installable) => {
153                cmd.arg(installable);
154
155                // try to detect if the flake is local
156                if let Some(Ok(local_flake)) = cwd
157                    .to_string_lossy()
158                    .split_once('#')
159                    .map(|(path, _)| path)
160                    .map(std::fs::canonicalize)
161                {
162                    // and if so, rerun if it changes
163                    println!(
164                        "cargo:rerun-if-changed={}",
165                        local_flake.join("flake.lock").display()
166                    );
167                }
168            }
169            NixTarget::Expr(expr) => {
170                cmd.args(["--expr", expr.as_str()]);
171            }
172        }
173
174        for (key, val) in &self.arg_exprs {
175            cmd.args(&["--arg", &key, &val]);
176        }
177
178        for (key, val) in &self.arg_strs {
179            cmd.args(&["--argstr", &key, &val]);
180        }
181
182        if self.impure {
183            cmd.arg("--impure");
184        }
185
186        //show build logs
187        cmd.arg("-L");
188
189        // enable split commands and flakes
190        cmd.args(&["--experimental-features", "nix-command flakes"]);
191
192        let output = cmd.output().map_err(|_| Error::BuildFailed)?;
193
194        if !output.status.success() {
195            return Err(Error::BuildFailed);
196        }
197
198        serde_json::from_slice(&output.stdout).map_err(|_| Error::UnknownOutput)
199    }
200}