1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
|
/*-
* SPDX-FileCopyrightText: Peter Pentchev <roam@ringlet.net>
* SPDX-License-Identifier: BSD-2-Clause
*/
use std::env;
use std::fs;
use std::io::ErrorKind;
use std::os::unix::fs::MetadataExt as _;
use std::path::Path;
use std::process::Command;
use anyhow::{bail, Context as _, Result};
use clap::Parser as _;
use clap_derive::Parser;
#[derive(Debug, Parser)]
#[clap(version)]
struct Cli {
/// Display the features supported by the program.
#[clap(long)]
features: bool,
/// Specify a reference file to obtain the information from.
#[clap(short)]
reffile: Option<String>,
/// Verbose operation; display diagnostic output.
#[clap(short, long)]
verbose: bool,
filenames: Vec<String>,
}
const VERSION_STR: &str = env!("CARGO_PKG_VERSION");
struct Config {
filenames: Vec<String>,
destination: String,
refname: Option<String>,
verbose: bool,
}
enum Mode {
Handled,
Install(Config),
}
#[expect(clippy::print_stdout, reason = "This is the purpose of this function")]
fn features() {
println!("Features: install-mimic={VERSION_STR}");
}
fn install_mimic<SP: AsRef<Path>, DP: AsRef<Path>>(
src: SP,
dst: DP,
refname: Option<&str>,
verbose: bool,
) -> Result<()> {
let src_path = src.as_ref().to_str().with_context(|| {
format!(
"Could not build a source path from {src}",
src = src.as_ref().display()
)
})?;
let dst_path = dst.as_ref().to_str().with_context(|| {
format!(
"Could not build a destination path from {dst}",
dst = dst.as_ref().display()
)
})?;
let filetoref = refname.map_or_else(|| dst_path.to_owned(), ToOwned::to_owned);
let stat =
fs::metadata(&filetoref).with_context(|| format!("Could not examine {filetoref}"))?;
let user_id = stat.uid().to_string();
let group_id = stat.gid().to_string();
let mode = format!("{mode:o}", mode = stat.mode() & 0o7777);
let prog_name = "install";
let args = [
"-c", "-o", &user_id, "-g", &group_id, "-m", &mode, "--", src_path, dst_path,
];
let mut cmd = Command::new(prog_name);
cmd.args(args);
#[expect(clippy::print_stdout, reason = "This is the purpose of this function")]
if verbose {
println!("{prog_name} {args}", args = shell_words::join(args));
}
if !cmd.status().context("Could not run install")?.success() {
bail!("Could not install {src_path} as {dst_path}");
}
Ok(())
}
fn parse_args() -> Result<Mode> {
let opts = Cli::parse();
if opts.features {
features();
return Ok(Mode::Handled);
}
let mut filenames = opts.filenames;
let destination = filenames
.pop()
.context("No source or destination paths specified")?;
if filenames.is_empty() {
bail!("At least one source and one destination path must be specified");
}
Ok(Mode::Install(Config {
filenames,
destination,
refname: opts.reffile,
verbose: opts.verbose,
}))
}
fn doit(cfg: &Config) -> Result<()> {
let is_dir = match fs::metadata(&cfg.destination) {
Err(err) if err.kind() == ErrorKind::NotFound => {
if cfg.refname.is_none() {
bail!(
"The destination path {dst} does not exist and no -r specified",
dst = cfg.destination
);
}
false
}
Err(err) => {
bail!("Could not examine {dst}: {err}", dst = cfg.destination);
}
Ok(data) => data.is_dir(),
};
if is_dir {
let dstpath: &Path = cfg.destination.as_ref();
for path in &cfg.filenames {
let pathref: &Path = path.as_ref();
let basename = pathref
.file_name()
.with_context(|| format!("Invalid source filename {path}"))?;
install_mimic(
path,
dstpath.join(basename),
cfg.refname.as_deref(),
cfg.verbose,
)?;
}
Ok(())
} else {
match *cfg.filenames {
[ref source] => install_mimic(source, &cfg.destination, cfg.refname.as_deref(), cfg.verbose),
_ => bail!("The destination path must be a directory if more than one source path is specified"),
}
}
}
fn main() -> Result<()> {
match parse_args().context("Could not parse the command-line arguments")? {
Mode::Handled => Ok(()),
Mode::Install(cfg) => doit(&cfg),
}
}
|