|
1 |
| -#[warn(clippy::cargo)] |
2 | 1 | mod builder;
|
3 | 2 | mod clean_path;
|
4 | 3 | mod derivation;
|
| 4 | +mod opts; |
5 | 5 |
|
6 |
| -use crate::builder::Builder; |
7 |
| -use anyhow::{Context, Result}; |
8 | 6 | use clap::Parser;
|
9 |
| -use clean_path::clean_path; |
10 |
| -use fs2::FileExt; |
11 |
| -use nix_script_directives::expr::Expr; |
12 |
| -use nix_script_directives::Directives; |
13 |
| -use std::env; |
14 |
| -use std::fs::{self, File}; |
15 |
| -use std::io::ErrorKind; |
16 |
| -use std::os::unix::fs::symlink; |
17 |
| -use std::os::unix::process::ExitStatusExt; |
18 |
| -use std::path::{Path, PathBuf}; |
19 |
| -use std::process::{Command, ExitStatus}; |
20 |
| - |
21 |
| -// TODO: Options for the rest of the directives. |
22 |
| -#[derive(Debug, Parser)] |
23 |
| -#[clap(version, trailing_var_arg = true)] |
24 |
| -struct Opts { |
25 |
| - /// What indicator do directives start with in the source file? |
26 |
| - #[clap(long, default_value = "#!")] |
27 |
| - indicator: String, |
28 |
| - |
29 |
| - /// How should we build this script? (Will override any `#!build` line |
30 |
| - /// present in the script.) |
31 |
| - #[clap(long)] |
32 |
| - build_command: Option<String>, |
33 |
| - |
34 |
| - /// Add build inputs to those specified by the source directives. |
35 |
| - #[clap(long("build-input"))] |
36 |
| - build_inputs: Vec<String>, |
37 |
| - |
38 |
| - /// Run the script by passing it to this interpreter instead of running |
39 |
| - /// the compiled binary directly. The interpreter must be included via some |
40 |
| - /// runtime input. |
41 |
| - #[clap(long("interpreter"))] |
42 |
| - interpreter: Option<String>, |
43 |
| - |
44 |
| - /// Add runtime inputs to those specified by the source directives. |
45 |
| - #[clap(long("runtime-input"))] |
46 |
| - runtime_inputs: Vec<String>, |
47 |
| - |
48 |
| - /// Override the configuration that will be passed to nixpkgs on import. |
49 |
| - #[clap( |
50 |
| - long("nixpkgs-config"), |
51 |
| - value_parser = clap::value_parser!(Expr), |
52 |
| - env("NIX_SCRIPT_NIXPKGS_CONFIG") |
53 |
| - )] |
54 |
| - nixpkgs_config: Option<Expr>, |
55 |
| - |
56 |
| - /// Instead of executing the script, parse directives from the file and |
57 |
| - /// print them as JSON to stdout. |
58 |
| - #[clap(long("parse"), conflicts_with_all(&["export", "shell"]))] |
59 |
| - parse: bool, |
60 |
| - |
61 |
| - /// Instead of executing the script, print the derivation we'd build |
62 |
| - ////to stdout. |
63 |
| - #[clap(long("export"), conflicts_with_all(&["parse", "shell"]))] |
64 |
| - export: bool, |
65 |
| - |
66 |
| - /// Enter a shell with the build-time and runtime inputs available. |
67 |
| - #[clap(long, conflicts_with_all(&["parse", "export"]))] |
68 |
| - shell: bool, |
69 |
| - |
70 |
| - /// In shell mode, run this command instead of a shell. |
71 |
| - #[clap(long, requires("shell"))] |
72 |
| - run: Option<String>, |
73 |
| - |
74 |
| - /// In shell mode, run a "pure" shell (that is, one that isolates the |
75 |
| - /// shell a little more from what you have in your environment.) |
76 |
| - #[clap(long, requires("shell"))] |
77 |
| - pure: bool, |
78 |
| - |
79 |
| - /// Use this folder as the root for any building we do. You can use this |
80 |
| - /// to bring other files into scope in your build. If there is a `default.nix` |
81 |
| - /// file in the specified root, we will use that instead of generating our own. |
82 |
| - #[clap(long)] |
83 |
| - build_root: Option<PathBuf>, |
84 |
| - |
85 |
| - /// Include files for use at runtime (relative to the build root). |
86 |
| - #[clap(long)] |
87 |
| - runtime_files: Vec<PathBuf>, |
88 |
| - |
89 |
| - /// Where should we cache files? |
90 |
| - #[clap(long("cache-directory"), env("NIX_SCRIPT_CACHE"))] |
91 |
| - cache_directory: Option<PathBuf>, |
92 |
| - |
93 |
| - /// The script to run (required), plus any arguments (optional). Any positional |
94 |
| - /// arguments after the script name will be passed on to the script. |
95 |
| - // Note: it'd be better to have a "script" and "args" field separately, |
96 |
| - // but there's a parsing issue in Clap (not a bug, but maybe a bug?) that |
97 |
| - // prevents passing args starting in -- after the script if we do that. See |
98 |
| - // https://github.com/clap-rs/clap/issues/1538 |
99 |
| - #[clap(num_args = 1.., required = true)] |
100 |
| - script_and_args: Vec<String>, |
101 |
| -} |
102 |
| - |
103 |
| -impl Opts { |
104 |
| - fn run(&self) -> Result<ExitStatus> { |
105 |
| - // First things first: what are we running? Where does it live? What |
106 |
| - // are its arguments? |
107 |
| - let (mut script, args) = self |
108 |
| - .parse_script_and_args() |
109 |
| - .context("could not parse script and args")?; |
110 |
| - script = clean_path(&script).context("could not clean path to script")?; |
111 |
| - |
112 |
| - if self.shell && !args.is_empty() { |
113 |
| - log::warn!("You specified both `--shell` and script args. I am going to ignore the args! Use `--run` if you want to run something in the shell immediately."); |
114 |
| - } |
115 |
| - |
116 |
| - let script_name = script |
117 |
| - .file_name() |
118 |
| - .context("script did not have a file name")? |
119 |
| - .to_str() |
120 |
| - .context("filename was not valid UTF-8")?; |
121 |
| - |
122 |
| - // Parse our directives, but don't combine them with command-line arguments yet! |
123 |
| - let mut directives = Directives::from_file(&self.indicator, &script) |
124 |
| - .context("could not parse directives from script")?; |
125 |
| - |
126 |
| - let mut build_root = self.build_root.to_owned(); |
127 |
| - if build_root.is_none() { |
128 |
| - if let Some(from_directives) = &directives.build_root { |
129 |
| - let out = script |
130 |
| - .parent() |
131 |
| - .map(Path::to_path_buf) |
132 |
| - .unwrap_or_else(|| PathBuf::from(".")); |
133 |
| - |
134 |
| - out.join(from_directives) |
135 |
| - .canonicalize() |
136 |
| - .context("could not canonicalize final path to build root")?; |
137 |
| - |
138 |
| - log::debug!("path to root from script directive: {}", out.display()); |
139 |
| - |
140 |
| - build_root = Some(out); |
141 |
| - } |
142 |
| - }; |
143 |
| - if build_root.is_none() |
144 |
| - && (!self.runtime_files.is_empty() || !directives.runtime_files.is_empty()) |
145 |
| - { |
146 |
| - log::warn!("Requested runtime files without specifying a build root. I am assuming it is the parent directory of the script for now, but you should set it explicitly!"); |
147 |
| - build_root = Some( |
148 |
| - script |
149 |
| - .parent() |
150 |
| - .map(|p| p.to_owned()) |
151 |
| - .unwrap_or_else(|| PathBuf::from(".")), |
152 |
| - ); |
153 |
| - } |
154 |
| - |
155 |
| - let mut builder = if let Some(build_root) = &build_root { |
156 |
| - Builder::from_directory(build_root, &script) |
157 |
| - .context("could not initialize source in directory")? |
158 |
| - } else { |
159 |
| - Builder::from_script(&script) |
160 |
| - }; |
161 |
| - |
162 |
| - // First place we might bail early: if a script just wants to parse |
163 |
| - // directives using our parser, we dump JSON and quit instead of running. |
164 |
| - if self.parse { |
165 |
| - println!( |
166 |
| - "{}", |
167 |
| - serde_json::to_string(&directives).context("could not serialize directives")? |
168 |
| - ); |
169 |
| - return Ok(ExitStatus::from_raw(0)); |
170 |
| - } |
171 |
| - |
172 |
| - // We don't merge command-line and script directives until now because |
173 |
| - // we shouldn't provide them in the output of `--parse` without showing |
174 |
| - // where each option came from. For now, we're assuming that people who |
175 |
| - // write wrapper scripts know what they want to pass into `nix-script`. |
176 |
| - directives.maybe_override_build_command(&self.build_command); |
177 |
| - directives |
178 |
| - .merge_build_inputs(&self.build_inputs) |
179 |
| - .context("could not add build inputs provided on the command line")?; |
180 |
| - if let Some(interpreter) = &self.interpreter { |
181 |
| - directives.override_interpreter(interpreter) |
182 |
| - } |
183 |
| - directives |
184 |
| - .merge_runtime_inputs(&self.runtime_inputs) |
185 |
| - .context("could not add runtime inputs provided on the command line")?; |
186 |
| - directives.merge_runtime_files(&self.runtime_files); |
187 |
| - if let Some(expr) = &self.nixpkgs_config { |
188 |
| - directives |
189 |
| - .override_nixpkgs_config(expr) |
190 |
| - .context("could not set nixpkgs config provided on the command line")?; |
191 |
| - } |
192 |
| - |
193 |
| - // Second place we might bail early: if we're requesting a shell instead |
194 |
| - // of building and running the script. |
195 |
| - if self.shell { |
196 |
| - return self.run_shell(script, &directives); |
197 |
| - } |
198 |
| - |
199 |
| - // Third place we can bail early: if someone wants the generated |
200 |
| - // derivation to do IFD or similar. |
201 |
| - if self.export { |
202 |
| - // We check here instead of inside while isolating the script or |
203 |
| - // similar so we can get an early bail that doesn't create trash |
204 |
| - // in the system's temporary directories. |
205 |
| - if build_root.is_none() { |
206 |
| - anyhow::bail!( |
207 |
| - "I do not have a root to refer to while exporting, so I cannot isolate the script and dependencies. Specify a --build-root and try this again!" |
208 |
| - ) |
209 |
| - } |
210 |
| - |
211 |
| - println!( |
212 |
| - "{}", |
213 |
| - builder |
214 |
| - .derivation(&directives, true) |
215 |
| - .context("could not create a Nix derivation from the script")? |
216 |
| - ); |
217 |
| - return Ok(ExitStatus::from_raw(0)); |
218 |
| - } |
219 |
| - |
220 |
| - let cache_directory = self |
221 |
| - .get_cache_directory() |
222 |
| - .context("could not get cache directory")?; |
223 |
| - log::debug!( |
224 |
| - "using `{}` as the cache directory", |
225 |
| - cache_directory.display() |
226 |
| - ); |
227 |
| - |
228 |
| - // Create hash, check cache. |
229 |
| - let hash = builder |
230 |
| - .hash(&directives) |
231 |
| - .context("could not calculate cache location for the compiled versoin of the script")?; |
232 |
| - |
233 |
| - let target_unique_id = format!("{hash}-{script_name}"); |
234 |
| - let target = cache_directory.join(target_unique_id.clone()); |
235 |
| - log::trace!("cache target: {}", target.display()); |
236 |
| - |
237 |
| - // Before we perform the build, we need to check if the symlink target |
238 |
| - // has gone stale. This can happen when you run `nix-collect-garbage`, |
239 |
| - // since we don't pin the resulting derivations. We have to do things |
240 |
| - // in a slightly less ergonomic way in order to not follow symlinks. |
241 |
| - if fs::symlink_metadata(&target).is_ok() { |
242 |
| - let link_target = fs::read_link(&target).context("failed to read existing symlink")?; |
243 |
| - |
244 |
| - if !link_target.exists() { |
245 |
| - log::info!("removing stale (garbage-collected?) symlink"); |
246 |
| - fs::remove_file(&target).context("could not remove stale symlink")?; |
247 |
| - } |
248 |
| - } |
249 |
| - |
250 |
| - if !target.exists() { |
251 |
| - log::debug!("hashed path does not exist; building"); |
252 |
| - |
253 |
| - // Initialize build lock. |
254 |
| - // |
255 |
| - // We lock the build after checking for the target. This has the |
256 |
| - // advantage that all subsequent executions will not bother with |
257 |
| - // creating lock files and obtaining locks. However, it has the |
258 |
| - // disadvantage that we always move on to building the derivation, |
259 |
| - // even when another builder has done the job for us in the |
260 |
| - // meantime. |
261 |
| - let lock_file_path = env::temp_dir().join(target_unique_id); |
262 |
| - log::debug!("creating lock file path: {:?}", lock_file_path); |
263 |
| - let lock_file = |
264 |
| - File::create(lock_file_path.clone()).context("could not create lock file")?; |
265 |
| - log::debug!("locking"); |
266 |
| - // Obtain lock. |
267 |
| - // TODO: Obtain lock with timeout. |
268 |
| - lock_file |
269 |
| - .lock_exclusive() |
270 |
| - .context("could not obtain lock")?; |
271 |
| - log::debug!("obtained lock"); |
272 |
| - |
273 |
| - let out_path = builder |
274 |
| - .build(&cache_directory, &hash, &directives) |
275 |
| - .context("could not build derivation from script")?; |
276 |
| - |
277 |
| - if let Err(err) = symlink(out_path, &target) { |
278 |
| - match err.kind() { |
279 |
| - ErrorKind::AlreadyExists => { |
280 |
| - // We could hypothetically detect if the link is |
281 |
| - // pointing to the right location, but the Nix paths |
282 |
| - // change for minor reasons that don't matter for script |
283 |
| - // execution. Instead, we just warn here and trust our |
284 |
| - // cache key to do the right thing. If we get a |
285 |
| - // collision, we do! |
286 |
| - log::warn!("detected a parallel write to the cache"); |
287 |
| - } |
288 |
| - _ => return Err(err).context("could not create symlink in cache"), |
289 |
| - } |
290 |
| - } |
291 |
| - |
292 |
| - // Make sure that we remove the temporary build directory before releasing the lock. |
293 |
| - drop(builder); |
294 |
| - // Release lock. |
295 |
| - log::debug!("releasing lock"); |
296 |
| - lock_file.unlock().context("could not release lock")?; |
297 |
| - // Do not remove the lock file because other tasks may still be |
298 |
| - // waiting for obtaining a lock on the file. |
299 |
| - } else { |
300 |
| - log::debug!("hashed path exists; skipping build"); |
301 |
| - } |
302 |
| - |
303 |
| - let mut child = Command::new(target.join("bin").join(script_name)) |
304 |
| - .args(args) |
305 |
| - .spawn() |
306 |
| - .context("could not start the script")?; |
307 |
| - |
308 |
| - child.wait().context("could not run the script") |
309 |
| - } |
310 |
| - |
311 |
| - fn parse_script_and_args(&self) -> Result<(PathBuf, Vec<String>)> { |
312 |
| - log::trace!("parsing script and args"); |
313 |
| - let mut script_and_args = self.script_and_args.iter(); |
314 |
| - |
315 |
| - let script = PathBuf::from( |
316 |
| - script_and_args |
317 |
| - .next() |
318 |
| - .context("no script name; this is a bug; please report")?, |
319 |
| - ); |
320 |
| - |
321 |
| - Ok((script, self.script_and_args[1..].to_vec())) |
322 |
| - } |
323 |
| - |
324 |
| - fn get_cache_directory(&self) -> Result<PathBuf> { |
325 |
| - let mut target = match &self.cache_directory { |
326 |
| - Some(explicit) => explicit.to_owned(), |
327 |
| - None => { |
328 |
| - let dirs = directories::ProjectDirs::from("zone", "bytes", "nix-script").context( |
329 |
| - "couldn't load HOME (set --cache-directory explicitly to get around this.)", |
330 |
| - )?; |
331 |
| - |
332 |
| - dirs.cache_dir().to_owned() |
333 |
| - } |
334 |
| - }; |
335 |
| - |
336 |
| - if target.is_relative() { |
337 |
| - target = std::env::current_dir() |
338 |
| - .context("no the current directory while calculating absolute path to the cache")? |
339 |
| - .join(target) |
340 |
| - } |
341 |
| - |
342 |
| - if !target.exists() { |
343 |
| - log::trace!("creating cache directory"); |
344 |
| - std::fs::create_dir_all(&target).context("could not create cache directory")?; |
345 |
| - } |
346 |
| - |
347 |
| - Ok(target) |
348 |
| - } |
349 |
| - |
350 |
| - fn run_shell(&self, script_file: PathBuf, directives: &Directives) -> Result<ExitStatus> { |
351 |
| - log::debug!("entering shell mode"); |
352 |
| - |
353 |
| - let mut command = Command::new("nix-shell"); |
354 |
| - |
355 |
| - log::trace!("setting SCRIPT_FILE to `{}`", script_file.display()); |
356 |
| - command.env("SCRIPT_FILE", script_file); |
357 |
| - |
358 |
| - if self.pure { |
359 |
| - log::trace!("setting shell to pure mode"); |
360 |
| - command.arg("--pure"); |
361 |
| - } |
362 |
| - |
363 |
| - for input in &directives.build_inputs { |
364 |
| - log::trace!("adding build input `{}` to packages", input); |
365 |
| - command.arg("-p").arg(input.to_string()); |
366 |
| - } |
367 |
| - |
368 |
| - for input in &directives.runtime_inputs { |
369 |
| - log::trace!("adding runtime input `{}` to packages", input); |
370 |
| - command.arg("-p").arg(input.to_string()); |
371 |
| - } |
372 |
| - |
373 |
| - if let Some(run) = &self.run { |
374 |
| - log::trace!("running `{}`", run); |
375 |
| - command.arg("--run").arg(run); |
376 |
| - } |
377 |
| - |
378 |
| - command |
379 |
| - .spawn() |
380 |
| - .context("could not start nix-shell")? |
381 |
| - .wait() |
382 |
| - .context("could not start the shell") |
383 |
| - } |
384 |
| -} |
| 7 | +use opts::Opts; |
385 | 8 |
|
386 | 9 | fn main() {
|
387 | 10 | env_logger::Builder::from_env("NIX_SCRIPT_LOG").init();
|
|
0 commit comments