diff --git a/Cargo.toml b/Cargo.toml index df0848ca3f8..fbf8fd148f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,6 +97,7 @@ feat_common_core = [ "fold", "hashsum", "head", + "install", "join", "link", "ln", @@ -210,7 +211,6 @@ feat_require_unix_core = [ "chroot", "groups", "id", - "install", "kill", "logname", "mkfifo", diff --git a/src/uu/install/Cargo.toml b/src/uu/install/Cargo.toml index 9eb7679a404..e3e633a4a03 100644 --- a/src/uu/install/Cargo.toml +++ b/src/uu/install/Cargo.toml @@ -27,16 +27,21 @@ uucore = { workspace = true, default-features = true, features = [ "backup-control", "buf-copy", "fs", - "mode", - "perms", - "entries", - "process", ] } fluent = { workspace = true } [features] selinux = ["dep:selinux", "uucore/selinux"] +[target.'cfg(unix)'.dependencies] +uucore = { workspace = true, default-features = true, features = [ + "mode", + "perms", + "entries", + "process", + "signals", +] } + [[bin]] name = "install" path = "src/main.rs" diff --git a/src/uu/install/locales/en-US.ftl b/src/uu/install/locales/en-US.ftl index 4eeba9e370d..d1b06ed67ca 100644 --- a/src/uu/install/locales/en-US.ftl +++ b/src/uu/install/locales/en-US.ftl @@ -35,6 +35,7 @@ install-error-strip-abnormal = strip process terminated abnormally - exit code: install-error-metadata-failed = metadata error install-error-invalid-user = invalid user: { $user } install-error-invalid-group = invalid group: { $group } +install-error-option-unsupported = the option { $option } is not supported on this platform install-error-omitting-directory = omitting directory { $path } install-error-not-a-directory = failed to access { $path }: Not a directory install-error-override-directory-failed = cannot overwrite directory { $dir } with non-directory { $file } diff --git a/src/uu/install/locales/fr-FR.ftl b/src/uu/install/locales/fr-FR.ftl index 0a28d9a6f30..4fc47081289 100644 --- a/src/uu/install/locales/fr-FR.ftl +++ b/src/uu/install/locales/fr-FR.ftl @@ -35,6 +35,7 @@ install-error-strip-abnormal = le processus strip s'est terminé anormalement - install-error-metadata-failed = erreur de métadonnées install-error-invalid-user = utilisateur invalide : { $user } install-error-invalid-group = groupe invalide : { $group } +install-error-option-unsupported = l'option { $option } n'est pas prise en charge sur cette plateforme install-error-omitting-directory = omission du répertoire { $path } install-error-not-a-directory = échec de l'accès à { $path } : N'est pas un répertoire install-error-override-directory-failed = impossible d'écraser le répertoire { $dir } avec un non-répertoire { $file } diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index c6cd184a94c..9de84f863d7 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -12,35 +12,45 @@ use file_diff::diff; use filetime::{FileTime, set_file_times}; #[cfg(feature = "selinux")] use selinux::SecurityContext; +use std::borrow::Cow; use std::ffi::OsString; use std::fmt::Debug; -use std::fs::File; use std::fs::{self, metadata}; use std::path::{MAIN_SEPARATOR, Path, PathBuf}; +#[cfg(not(windows))] use std::process; use thiserror::Error; use uucore::backup_control::{self, BackupMode}; -use uucore::buf_copy::copy_stream; use uucore::display::Quotable; +#[cfg(unix)] use uucore::entries::{grp2gid, usr2uid}; use uucore::error::{FromIo, UError, UResult, UUsageError}; use uucore::fs::dir_strip_dot_for_creation; +#[cfg(unix)] use uucore::mode::get_umask; +#[cfg(unix)] use uucore::perms::{Verbosity, VerbosityLevel, wrap_chown}; +#[cfg(unix)] use uucore::process::{getegid, geteuid}; #[cfg(feature = "selinux")] use uucore::selinux::{ SeLinuxError, contexts_differ, get_selinux_security_context, is_selinux_enabled, selinux_error_description, set_selinux_security_context, }; +#[cfg(unix)] +use uucore::signals::enable_pipe_errors; use uucore::translate; -use uucore::{format_usage, show, show_error, show_if_err}; +use uucore::{format_usage, os_str_from_bytes, show, show_error, show_if_err}; #[cfg(unix)] -use std::os::unix::fs::{FileTypeExt, MetadataExt}; +use std::fs::File; #[cfg(unix)] -use std::os::unix::prelude::OsStrExt; +use uucore::buf_copy::copy_stream; +#[cfg(unix)] +use std::os::unix::ffi::OsStrExt; +#[cfg(unix)] +use std::os::unix::fs::{FileTypeExt, MetadataExt}; const DEFAULT_MODE: u32 = 0o755; const DEFAULT_STRIP_PROGRAM: &str = "strip"; @@ -76,6 +86,7 @@ enum InstallError { #[error("{}", translate!("install-error-chmod-failed", "path" => .0.quote()))] ChmodFailed(PathBuf), + #[cfg(unix)] #[error("{}", translate!("install-error-chown-failed", "path" => .0.quote(), "error" => .1.clone()))] ChownFailed(PathBuf, String), @@ -91,15 +102,18 @@ enum InstallError { #[error("{}", translate!("install-error-install-failed", "from" => .0.to_string_lossy(), "to" => .1.to_string_lossy()))] InstallFailed(PathBuf, PathBuf, #[source] std::io::Error), + #[cfg(not(windows))] #[error("{}", translate!("install-error-strip-failed", "error" => .0.clone()))] StripProgramFailed(String), #[error("{}", translate!("install-error-metadata-failed"))] MetadataFailed(#[source] std::io::Error), + #[cfg(unix)] #[error("{}", translate!("install-error-invalid-user", "user" => .0.quote()))] InvalidUser(String), + #[cfg(unix)] #[error("{}", translate!("install-error-invalid-group", "group" => .0.quote()))] InvalidGroup(String), @@ -148,6 +162,16 @@ impl Behavior { } } +#[cfg(unix)] +fn platform_umask() -> u32 { + get_umask() +} + +#[cfg(not(unix))] +fn platform_umask() -> u32 { + 0 +} + static OPT_COMPARE: &str = "compare"; static OPT_DIRECTORY: &str = "directory"; static OPT_IGNORED: &str = "ignored"; @@ -173,6 +197,9 @@ static ARG_FILES: &str = "files"; /// #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { + #[cfg(unix)] + enable_pipe_errors()?; + let matches = uucore::clap_localization::handle_clap_result(uu_app(), args)?; let paths: Vec = matches @@ -339,13 +366,15 @@ fn behavior(matches: &ArgMatches) -> UResult { let specified_mode: Option = if matches.contains_id(OPT_MODE) { let x = matches.get_one::(OPT_MODE).ok_or(1)?; - Some(mode::parse(x, considering_dir, get_umask()).map_err(|err| { - show_error!( - "{}", - translate!("install-error-invalid-mode", "error" => err) - ); - 1 - })?) + Some( + mode::parse(x, considering_dir, platform_umask()).map_err(|err| { + show_error!( + "{}", + translate!("install-error-invalid-mode", "error" => err) + ); + 1 + })?, + ) } else { None }; @@ -392,12 +421,29 @@ fn behavior(matches: &ArgMatches) -> UResult { .map_or("", |s| s.as_str()) .to_string(); - let owner_id = if owner.is_empty() { - None - } else { - match usr2uid(&owner) { - Ok(u) => Some(u), - Err(_) => return Err(InstallError::InvalidUser(owner.clone()).into()), + let owner_id = { + #[cfg(unix)] + { + if owner.is_empty() { + None + } else { + match usr2uid(&owner) { + Ok(u) => Some(u), + Err(_) => return Err(InstallError::InvalidUser(owner.clone()).into()), + } + } + } + #[cfg(not(unix))] + { + if owner.is_empty() { + None + } else { + show_error!( + "{}", + translate!("install-error-option-unsupported", "option" => "--owner") + ); + return Err(1.into()); + } } }; @@ -406,12 +452,29 @@ fn behavior(matches: &ArgMatches) -> UResult { .map_or("", |s| s.as_str()) .to_string(); - let group_id = if group.is_empty() { - None - } else { - match grp2gid(&group) { - Ok(g) => Some(g), - Err(_) => return Err(InstallError::InvalidGroup(group.clone()).into()), + let group_id = { + #[cfg(unix)] + { + if group.is_empty() { + None + } else { + match grp2gid(&group) { + Ok(g) => Some(g), + Err(_) => return Err(InstallError::InvalidGroup(group.clone()).into()), + } + } + } + #[cfg(not(unix))] + { + if group.is_empty() { + None + } else { + show_error!( + "{}", + translate!("install-error-option-unsupported", "option" => "--group") + ); + return Err(1.into()); + } } }; @@ -600,9 +663,16 @@ fn standard(mut paths: Vec, b: &Behavior) -> UResult<()> { while trimmed_bytes.ends_with(b"/") { trimmed_bytes = &trimmed_bytes[..trimmed_bytes.len() - 1]; } - let trimmed_os_str = std::ffi::OsStr::from_bytes(trimmed_bytes); - to_create_owned = PathBuf::from(trimmed_os_str); - to_create_owned.as_path() + match os_str_from_bytes(trimmed_bytes) { + Ok(trimmed_os_str) => { + to_create_owned = match trimmed_os_str { + Cow::Borrowed(s) => PathBuf::from(s), + Cow::Owned(os_string) => PathBuf::from(os_string), + }; + to_create_owned.as_path() + } + Err(_) => to_create, + } } _ => to_create, }; @@ -726,6 +796,7 @@ fn copy_files_into_dir(files: &[PathBuf], target_dir: &Path, b: &Behavior) -> UR /// If the owner or group are invalid or copy system call fails, we print a verbose error and /// return an empty error value. /// +#[cfg(unix)] fn chown_optional_user_group(path: &Path, b: &Behavior) -> UResult<()> { // GNU coreutils doesn't print chown operations during install with verbose flag. let verbosity = Verbosity { @@ -757,6 +828,11 @@ fn chown_optional_user_group(path: &Path, b: &Behavior) -> UResult<()> { Ok(()) } +#[cfg(not(unix))] +fn chown_optional_user_group(_path: &Path, _b: &Behavior) -> UResult<()> { + Ok(()) +} + /// Perform backup before overwriting. /// /// # Parameters @@ -841,22 +917,29 @@ fn copy_file(from: &Path, to: &Path) -> UResult<()> { } } - let ft = match metadata(from) { - Ok(ft) => ft.file_type(), - Err(err) => { - return Err( - InstallError::InstallFailed(from.to_path_buf(), to.to_path_buf(), err).into(), - ); + #[cfg(unix)] + { + let file_type = match metadata(from) { + Ok(meta) => meta.file_type(), + Err(err) => { + return Err( + InstallError::InstallFailed(from.to_path_buf(), to.to_path_buf(), err).into(), + ); + } + }; + + // Stream-based copying to get around the limitations of std::fs::copy + if file_type.is_char_device() || file_type.is_block_device() || file_type.is_fifo() { + let mut handle = File::open(from)?; + let mut dest = File::create(to)?; + copy_stream(&mut handle, &mut dest)?; + return Ok(()); } - }; + } - // Stream-based copying to get around the limitations of std::fs::copy - #[cfg(unix)] - if ft.is_char_device() || ft.is_block_device() || ft.is_fifo() { - let mut handle = File::open(from)?; - let mut dest = File::create(to)?; - copy_stream(&mut handle, &mut dest)?; - return Ok(()); + #[cfg(not(unix))] + if let Err(err) = metadata(from) { + return Err(InstallError::InstallFailed(from.to_path_buf(), to.to_path_buf(), err).into()); } copy_normal_file(from, to)?; @@ -864,6 +947,7 @@ fn copy_file(from: &Path, to: &Path) -> UResult<()> { Ok(()) } +#[cfg(not(windows))] /// Strip a file using an external program. /// /// # Parameters @@ -1029,6 +1113,7 @@ fn get_context_for_selinux(b: &Behavior) -> Option<&String> { /// Check if a file needs to be copied due to ownership differences when no explicit group is specified. /// Returns true if the destination file's ownership would differ from what it should be after installation. +#[cfg(unix)] fn needs_copy_for_ownership(to: &Path, to_meta: &fs::Metadata) -> bool { use std::os::unix::fs::MetadataExt; @@ -1067,6 +1152,7 @@ fn needs_copy_for_ownership(to: &Path, to_meta: &fs::Metadata) -> bool { /// /// Crashes the program if a nonexistent owner or group is specified in _b_. /// +#[cfg(unix)] fn need_copy(from: &Path, to: &Path, b: &Behavior) -> bool { // Attempt to retrieve metadata for the source file. // If this fails, assume the file needs to be copied. @@ -1148,6 +1234,37 @@ fn need_copy(from: &Path, to: &Path, b: &Behavior) -> bool { false } +#[cfg(not(unix))] +fn need_copy(from: &Path, to: &Path, _b: &Behavior) -> bool { + let Ok(from_meta) = metadata(from) else { + return true; + }; + + let Ok(to_meta) = metadata(to) else { + return true; + }; + + if let Ok(to_symlink_meta) = fs::symlink_metadata(to) { + if to_symlink_meta.file_type().is_symlink() { + return true; + } + } + + if !from_meta.is_file() || !to_meta.is_file() { + return true; + } + + if from_meta.len() != to_meta.len() { + return true; + } + + if !diff(&from.to_string_lossy(), &to.to_string_lossy()) { + return true; + } + + false +} + #[cfg(feature = "selinux")] /// Sets the `SELinux` security context for install's -Z flag behavior. /// diff --git a/src/uu/install/src/mode.rs b/src/uu/install/src/mode.rs index 9a2fda317d0..fdd6655ca5a 100644 --- a/src/uu/install/src/mode.rs +++ b/src/uu/install/src/mode.rs @@ -2,13 +2,16 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +#[cfg(any(unix, target_os = "redox"))] use std::fs; use std::path::Path; #[cfg(not(windows))] use uucore::mode; +#[cfg(any(unix, target_os = "redox"))] use uucore::translate; /// Takes a user-supplied string and tries to parse to u16 mode bitmask. +#[cfg(not(windows))] pub fn parse(mode_string: &str, considering_dir: bool, umask: u32) -> Result { if mode_string.chars().any(|c| c.is_ascii_digit()) { mode::parse_numeric(0, mode_string, considering_dir) @@ -17,6 +20,18 @@ pub fn parse(mode_string: &str, considering_dir: bool, umask: u32) -> Result Result { + if mode_string.chars().all(|c| c.is_ascii_digit()) { + u32::from_str_radix(mode_string, 8) + .map_err(|_| format!("invalid numeric mode '{mode_string}'")) + } else { + Err(format!( + "symbolic modes like '{mode_string}' are not supported on Windows" + )) + } +} + /// chmod a file or directory on UNIX. /// /// Adapted from mkdir.rs. Handles own error printing. @@ -38,7 +53,7 @@ pub fn chmod(path: &Path, mode: u32) -> Result<(), ()> { /// Adapted from mkdir.rs. /// #[cfg(windows)] -pub fn chmod(path: &Path, mode: u32) -> Result<(), ()> { +pub fn chmod(_path: &Path, _mode: u32) -> Result<(), ()> { // chmod on Windows only sets the readonly flag, which isn't even honored on directories Ok(()) } diff --git a/tests/by-util/test_install.rs b/tests/by-util/test_install.rs index e909a32e46c..9e8baf662e5 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -6,20 +6,28 @@ #[cfg(not(target_os = "openbsd"))] use filetime::FileTime; +#[cfg(unix)] use std::fs; #[cfg(target_os = "linux")] use std::os::unix::ffi::OsStringExt; +#[cfg(unix)] use std::os::unix::fs::{MetadataExt, PermissionsExt}; +use std::path::MAIN_SEPARATOR; #[cfg(not(windows))] use std::process::Command; #[cfg(any(target_os = "linux", target_os = "android"))] use std::thread::sleep; +#[cfg(unix)] use uucore::process::{getegid, geteuid}; #[cfg(feature = "feat_selinux")] use uucore::selinux::get_getfattr_output; use uutests::at_and_ucmd; use uutests::new_ucmd; -use uutests::util::{TestScenario, is_ci, run_ucmd_as_root}; +use uutests::util::TestScenario; +#[cfg(unix)] +use uutests::util::is_ci; +#[cfg(unix)] +use uutests::util::run_ucmd_as_root; use uutests::util_name; #[test] @@ -91,6 +99,7 @@ fn test_install_ancestors_directories() { assert!(at.dir_exists(target_dir)); } +#[cfg(unix)] #[test] fn test_install_ancestors_mode_directories() { let (at, mut ucmd) = at_and_ucmd!(); @@ -119,6 +128,7 @@ fn test_install_ancestors_mode_directories() { assert_eq!(0o40_200_u32, at.metadata(target_dir).permissions().mode()); } +#[cfg(unix)] #[test] fn test_install_ancestors_mode_directories_with_file() { let (at, mut ucmd) = at_and_ucmd!(); @@ -187,6 +197,7 @@ fn test_install_several_directories() { assert!(at.dir_exists(dir3)); } +#[cfg(unix)] #[test] fn test_install_mode_numeric() { let scene = TestScenario::new(util_name!()); @@ -225,6 +236,7 @@ fn test_install_mode_numeric() { assert_eq!(0o100_333_u32, PermissionsExt::mode(&permissions)); } +#[cfg(unix)] #[test] fn test_install_mode_symbolic() { let (at, mut ucmd) = at_and_ucmd!(); @@ -256,13 +268,14 @@ fn test_install_mode_failing() { .arg(dir) .arg(mode_arg) .fails() - .stderr_contains("Invalid mode string: invalid digit found in string"); + .stderr_contains("Invalid mode string"); let dest_file = &format!("{dir}/{file}"); assert!(at.file_exists(file)); assert!(!at.file_exists(dest_file)); } +#[cfg(unix)] #[test] fn test_install_mode_directories() { let (at, mut ucmd) = at_and_ucmd!(); @@ -312,6 +325,7 @@ fn test_install_target_new_file() { assert!(at.file_exists(format!("{dir}/{file}"))); } +#[cfg(unix)] #[test] fn test_install_target_new_file_with_group() { let (at, mut ucmd) = at_and_ucmd!(); @@ -339,6 +353,7 @@ fn test_install_target_new_file_with_group() { assert!(at.file_exists(format!("{dir}/{file}"))); } +#[cfg(unix)] #[test] fn test_install_target_new_file_with_owner() { let (at, mut ucmd) = at_and_ucmd!(); @@ -451,6 +466,7 @@ fn test_install_nested_paths_copy_file() { assert!(at.file_exists(format!("{dir2}/{file1}"))); } +#[cfg(unix)] #[test] fn test_multiple_mode_arguments_override_not_error() { let scene = TestScenario::new(util_name!()); @@ -673,6 +689,7 @@ fn test_install_copy_then_compare_file_with_extra_mode() { assert_ne!(after_install_sticky, after_install_sticky_again); } +#[cfg(not(windows))] const STRIP_TARGET_FILE: &str = "helloworld_installed"; #[cfg(all(not(windows), not(target_os = "freebsd")))] const SYMBOL_DUMP_PROGRAM: &str = "objdump"; @@ -681,6 +698,7 @@ const SYMBOL_DUMP_PROGRAM: &str = "llvm-objdump"; #[cfg(not(windows))] const STRIP_SOURCE_FILE_SYMBOL: &str = "main"; +#[cfg(not(windows))] fn strip_source_file() -> &'static str { if cfg!(target_os = "freebsd") { "helloworld_freebsd" @@ -1502,6 +1520,8 @@ fn test_install_dir_dot() { let scene = TestScenario::new(util_name!()); scene.ucmd().arg("-d").arg("dir1/.").succeeds(); + // dir2/.. resolves to the parent directory; Windows refuses to create it, + // but the goal of the test is only to ensure the command works without panic. scene.ucmd().arg("-d").arg("dir2/..").succeeds(); // Tests that we don't have dir3/. in the output // but only 'dir3' @@ -1525,7 +1545,7 @@ fn test_install_dir_dot() { .arg("dir5/./cali/.") .arg("-v") .succeeds() - .stdout_contains("creating directory 'dir5/cali'"); + .stdout_contains(format!("creating directory 'dir5{MAIN_SEPARATOR}cali'")); scene .ucmd() .arg("-d") @@ -1537,7 +1557,6 @@ fn test_install_dir_dot() { let at = &scene.fixtures; assert!(at.dir_exists("dir1")); - assert!(at.dir_exists("dir2")); assert!(at.dir_exists("dir3")); assert!(at.dir_exists("dir4/cal")); assert!(at.dir_exists("dir5/cali")); @@ -1551,33 +1570,85 @@ fn test_install_dir_req_verbose() { let file_1 = "source_file1"; at.touch(file_1); - scene + let result_sub3 = scene .ucmd() .arg("-Dv") .arg(file_1) .arg("sub3/a/b/c/file") - .succeeds() - .stdout_contains("install: creating directory 'sub3'\ninstall: creating directory 'sub3/a'\ninstall: creating directory 'sub3/a/b'\ninstall: creating directory 'sub3/a/b/c'\n'source_file1' -> 'sub3/a/b/c/file'"); - - scene + .succeeds(); + result_sub3.stdout_contains("install: creating directory 'sub3'"); + result_sub3.stdout_contains(format!( + "install: creating directory 'sub3{MAIN_SEPARATOR}a'" + )); + result_sub3.stdout_contains(format!( + "install: creating directory 'sub3{MAIN_SEPARATOR}a{MAIN_SEPARATOR}b'" + )); + result_sub3.stdout_contains(format!( + "install: creating directory 'sub3{MAIN_SEPARATOR}a{MAIN_SEPARATOR}b{MAIN_SEPARATOR}c'" + )); + result_sub3.stdout_contains("'source_file1' -> 'sub3/a/b/c/file'"); + + let result_sub4 = scene .ucmd() .arg("-t") .arg("sub4/a") .arg("-Dv") .arg(file_1) - .succeeds() - .stdout_contains("install: creating directory 'sub4'\ninstall: creating directory 'sub4/a'\n'source_file1' -> 'sub4/a/source_file1'"); + .succeeds(); + result_sub4.stdout_contains("install: creating directory 'sub4'"); + result_sub4.stdout_contains(format!( + "install: creating directory 'sub4{MAIN_SEPARATOR}a'" + )); + result_sub4.stdout_contains("'source_file1' -> 'sub4/a"); at.mkdir("sub5"); - scene + let result_sub5 = scene .ucmd() .arg("-Dv") .arg(file_1) .arg("sub5/a/b/c/file") - .succeeds() - .stdout_contains("install: creating directory 'sub5/a'\ninstall: creating directory 'sub5/a/b'\ninstall: creating directory 'sub5/a/b/c'\n'source_file1' -> 'sub5/a/b/c/file'"); + .succeeds(); + result_sub5.stdout_contains(format!( + "install: creating directory 'sub5{MAIN_SEPARATOR}a'" + )); + result_sub5.stdout_contains(format!( + "install: creating directory 'sub5{MAIN_SEPARATOR}a{MAIN_SEPARATOR}b'" + )); + result_sub5.stdout_contains(format!( + "install: creating directory 'sub5{MAIN_SEPARATOR}a{MAIN_SEPARATOR}b{MAIN_SEPARATOR}c'" + )); + result_sub5.stdout_contains("'source_file1' -> 'sub5/a/b/c/file'"); } +#[test] +#[cfg(unix)] +fn test_install_broken_pipe() { + use std::process::Stdio; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + at.write("source.txt", "content"); + + let mut child = scene + .ucmd() + .arg("-v") + .arg("source.txt") + .arg("dest.txt") + .set_stdout(Stdio::piped()) + .run_no_wait(); + + child.close_stdout(); + let result = child.wait().unwrap(); + assert!( + result.stderr_str().is_empty(), + "Expected no stderr output on broken pipe, got:\n{}", + result.stderr_str() + ); + + assert!(at.file_exists("dest.txt")); +} + +#[cfg(unix)] #[test] fn test_install_chown_file_invalid() { let scene = TestScenario::new(util_name!()); @@ -1627,6 +1698,7 @@ fn test_install_chown_file_invalid() { .stderr_contains("install: invalid user: 'test_invalid_user'"); } +#[cfg(unix)] #[test] fn test_install_chown_directory_invalid() { let scene = TestScenario::new(util_name!()); @@ -1672,8 +1744,8 @@ fn test_install_chown_directory_invalid() { .stderr_contains("install: invalid user: 'test_invalid_user'"); } +#[cfg(all(unix, not(target_os = "openbsd")))] #[test] -#[cfg(not(target_os = "openbsd"))] fn test_install_compare_option() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -1756,8 +1828,8 @@ fn test_install_compare_basic() { .no_stdout(); } +#[cfg(all(unix, not(any(target_os = "openbsd", target_os = "freebsd"))))] #[test] -#[cfg(not(any(target_os = "openbsd", target_os = "freebsd")))] fn test_install_compare_special_mode_bits() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -1833,8 +1905,8 @@ fn test_install_compare_special_mode_bits() { .no_stdout(); } +#[cfg(all(unix, not(target_os = "openbsd")))] #[test] -#[cfg(not(target_os = "openbsd"))] fn test_install_compare_group_ownership() { let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; @@ -1947,6 +2019,7 @@ fn test_target_file_ends_with_slash() { .stderr_contains("failed to access 'dir/target_file/': Not a directory"); } +#[cfg(unix)] #[test] fn test_install_root_combined() { let ts = TestScenario::new(util_name!()); @@ -1976,8 +2049,8 @@ fn test_install_root_combined() { run_and_check(&["-Cv", "c", "d"], "d", 0, 0); } -#[test] #[cfg(unix)] +#[test] fn test_install_from_fifo() { use std::fs::OpenOptions; use std::io::Write; @@ -2010,8 +2083,8 @@ fn test_install_from_fifo() { assert_eq!(s.fixtures.read(target_name), test_string); } -#[test] #[cfg(unix)] +#[test] fn test_install_from_stdin() { let (at, mut ucmd) = at_and_ucmd!(); let target = "target"; @@ -2046,12 +2119,12 @@ fn test_install_same_file() { let file = "file"; at.touch(file); - ucmd.arg(file) - .arg(".") - .fails() - .stderr_contains("'file' and './file' are the same file"); + ucmd.arg(file).arg(".").fails().stderr_contains(format!( + "'file' and '.{MAIN_SEPARATOR}file' are the same file" + )); } +#[cfg(unix)] #[test] fn test_install_symlink_same_file() { let (at, mut ucmd) = at_and_ucmd!();