Skip to content

Commit 6aa9a52

Browse files
committed
Search for external commands
1 parent 41168a9 commit 6aa9a52

1 file changed

Lines changed: 76 additions & 3 deletions

File tree

src/bin/tectonic/v2cli.rs

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
//! Cargo, as compared to the classic "rustc-like" CLI.
66
77
use std::{
8-
convert::Infallible, env, ffi::OsString, io::Write, path::PathBuf, process, str::FromStr,
9-
sync::Arc,
8+
convert::Infallible, env, ffi::OsString, fs, io::Write, path::Path, path::PathBuf, process,
9+
str::FromStr, sync::Arc,
1010
};
1111
use structopt::{clap::AppSettings, StructOpt};
1212
use tectonic::{
@@ -22,6 +22,7 @@ use tectonic::{
2222
use tectonic_bridge_core::{SecuritySettings, SecurityStance};
2323
use tectonic_bundles::Bundle;
2424
use tectonic_docmodel::workspace::{Workspace, WorkspaceCreator};
25+
use tectonic_errors::prelude::anyhow;
2526
use tectonic_status_base::plain::PlainStatusBackend;
2627
use tokio::runtime;
2728
use watchexec::event::ProcessEnd;
@@ -171,8 +172,8 @@ enum Commands {
171172
/// Create a new document project
172173
New(NewCommand),
173174

174-
/// Initializes a new document in the current directory
175175
#[structopt(name = "init")]
176+
/// Initializes a new document in the current directory
176177
Init(InitCommand),
177178

178179
#[structopt(name = "show")]
@@ -182,6 +183,10 @@ enum Commands {
182183
#[structopt(name = "watch")]
183184
/// Watch input files and execute commands on change
184185
Watch(WatchCommand),
186+
187+
#[structopt(external_subcommand)]
188+
/// Runs the external command `tectonic-[command]` if one exists.
189+
External(Vec<String>),
185190
}
186191

187192
impl Commands {
@@ -195,6 +200,7 @@ impl Commands {
195200
Commands::Init(o) => o.customize(cc),
196201
Commands::Show(o) => o.customize(cc),
197202
Commands::Watch(o) => o.customize(cc),
203+
Commands::External(_) => {}
198204
}
199205
}
200206

@@ -208,6 +214,7 @@ impl Commands {
208214
Commands::Init(o) => o.execute(config, status),
209215
Commands::Show(o) => o.execute(config, status),
210216
Commands::Watch(o) => o.execute(config, status),
217+
Commands::External(args) => do_external(args),
211218
}
212219
}
213220
}
@@ -757,3 +764,69 @@ impl ShowUserCacheDirCommand {
757764
Ok(0)
758765
}
759766
}
767+
768+
#[cfg(unix)]
769+
/// On Unix, exec() to replace ourselves with the child process. This function
770+
/// *should* never return.
771+
fn exec_or_spawn(cmd: &mut process::Command) -> Result<i32> {
772+
use std::os::unix::process::CommandExt;
773+
774+
// exec() only returns an io::Error directly, since on success it never
775+
// returns; the following tomfoolery transforms it into our Result
776+
// machinery as desired.
777+
Err(cmd.exec().into())
778+
}
779+
780+
#[cfg(not(unix))]
781+
/// On other platforms, just run the process and wait for it.
782+
fn exec_or_spawn(cmd: &mut process::Command) -> Result<i32> {
783+
// code() can only return None on Unix when the subprocess was killed by a
784+
// signal. This function only runs if we're not on Unix, so we'll always
785+
// get Some.
786+
Ok(cmd.status()?.code().unwrap())
787+
}
788+
789+
#[cfg(unix)]
790+
fn is_executable<P: AsRef<Path>>(path: P) -> bool {
791+
use std::os::unix::prelude::*;
792+
fs::metadata(path)
793+
.map(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0)
794+
.unwrap_or(false)
795+
}
796+
797+
#[cfg(windows)]
798+
fn is_executable<P: AsRef<Path>>(path: P) -> bool {
799+
fs::metadata(path)
800+
.map(|metadata| metadata.is_file())
801+
.unwrap_or(false)
802+
}
803+
804+
fn search_directories() -> Vec<PathBuf> {
805+
let mut dirs = Vec::new();
806+
807+
if let Some(val) = env::var_os("PATH") {
808+
dirs.extend(env::split_paths(&val));
809+
}
810+
dirs
811+
}
812+
813+
#[allow(clippy::redundant_closure)]
814+
/// Run an external command by executing a subprocess.
815+
fn do_external(all_args: Vec<String>) -> Result<i32> {
816+
let (cmd, args) = all_args.split_first().unwrap();
817+
818+
let command_exe = format!("tectonic-{}{}", cmd, env::consts::EXE_SUFFIX);
819+
let path = search_directories()
820+
.iter()
821+
.map(|dir| dir.join(&command_exe))
822+
.find(|file| is_executable(file));
823+
824+
let command = path.ok_or_else(|| {
825+
anyhow!(
826+
"no internal or external subcommand `{0}` is available (install `tectonic-{0}`?)",
827+
cmd
828+
)
829+
})?;
830+
831+
exec_or_spawn(process::Command::new(command).args(args))
832+
}

0 commit comments

Comments
 (0)