Skip to content

Commit f570198

Browse files
committed
nix-script: extract opts module
1 parent c72dd15 commit f570198

File tree

3 files changed

+382
-379
lines changed

3 files changed

+382
-379
lines changed
File renamed without changes.

nix-script/src/main.rs

Lines changed: 2 additions & 379 deletions
Original file line numberDiff line numberDiff line change
@@ -1,387 +1,10 @@
1-
#[warn(clippy::cargo)]
21
mod builder;
32
mod clean_path;
43
mod derivation;
4+
mod opts;
55

6-
use crate::builder::Builder;
7-
use anyhow::{Context, Result};
86
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;
3858

3869
fn main() {
38710
env_logger::Builder::from_env("NIX_SCRIPT_LOG").init();

0 commit comments

Comments
 (0)