From bc8cb47920694b2b90ee76a6b9e699d7a06848c9 Mon Sep 17 00:00:00 2001 From: Justin Tracey Date: Fri, 9 May 2025 20:43:48 -0400 Subject: [PATCH] allow building multicall binary as dynamic library --- .github/workflows/CICD.yml | 53 +++++--- Cargo.lock | 1 + Cargo.toml | 10 +- GNUmakefile | 35 +++++- README.md | 39 +++++- build.rs | 8 +- src/bin/coreutils.rs | 243 ++--------------------------------- src/lib/coreutils.rs | 251 +++++++++++++++++++++++++++++++++++++ 8 files changed, 382 insertions(+), 258 deletions(-) create mode 100644 src/lib/coreutils.rs diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index 80b2fa24053..5af3ce990c3 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -5,7 +5,7 @@ name: CICD # spell-checker:ignore (jargon) SHAs deps dequote softprops subshell toolchain fuzzers dedupe devel profdata # spell-checker:ignore (people) Peltoche rivy dtolnay Anson dawidd # spell-checker:ignore (shell/tools) binutils choco clippy dmake dpkg esac fakeroot fdesc fdescfs gmake grcov halium lcov libclang libfuse libssl limactl mkdir nextest nocross pacman popd printf pushd redoxer rsync rustc rustfmt rustup shopt sccache utmpdump xargs -# spell-checker:ignore (misc) aarch alnum armhf bindir busytest coreutils defconfig DESTDIR gecos getenforce gnueabihf issuecomment maint manpages msys multisize noconfirm nullglob onexitbegin onexitend pell runtest Swatinem tempfile testsuite toybox uutils +# spell-checker:ignore (misc) aarch alnum armhf bindir busytest coreutils defconfig DESTDIR dylib gecos getenforce gnueabihf issuecomment maint manpages msys multisize noconfirm nullglob onexitbegin onexitend pell runtest Swatinem tempfile testsuite toybox uutils env: PROJECT_NAME: coreutils @@ -513,25 +513,25 @@ jobs: fail-fast: false matrix: job: - # - { os , target , cargo-options , default-features, features , use-cross , toolchain, skip-tests, workspace-tests, skip-package, skip-publish } - - { os: ubuntu-latest , target: arm-unknown-linux-gnueabihf , features: feat_os_unix_gnueabihf , use-cross: use-cross , skip-tests: true } - - { os: ubuntu-24.04-arm , target: aarch64-unknown-linux-gnu , features: feat_os_unix_gnueabihf } - - { os: ubuntu-latest , target: aarch64-unknown-linux-musl , features: feat_os_unix , use-cross: use-cross , skip-tests: true } - # - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: feat_selinux , use-cross: use-cross } - - { os: ubuntu-latest , target: i686-unknown-linux-gnu , features: "feat_os_unix,test_risky_names", use-cross: use-cross } - - { os: ubuntu-latest , target: i686-unknown-linux-musl , features: feat_os_unix , use-cross: use-cross } - - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: "feat_os_unix,test_risky_names", use-cross: use-cross } - - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: "feat_os_unix,uudoc" , use-cross: no, workspace-tests: true } - - { os: ubuntu-latest , target: x86_64-unknown-linux-musl , features: feat_os_unix , use-cross: use-cross } - - { os: ubuntu-latest , target: x86_64-unknown-redox , features: feat_os_unix_redox , use-cross: redoxer , skip-tests: true } - - { os: ubuntu-latest , target: wasm32-unknown-unknown , default-features: false, features: uucore/format, skip-tests: true, skip-package: true, skip-publish: true } - - { os: macos-latest , target: aarch64-apple-darwin , features: feat_os_macos, workspace-tests: true } # M1 CPU - - { os: macos-13 , target: x86_64-apple-darwin , features: feat_os_macos, workspace-tests: true } - - { os: windows-latest , target: i686-pc-windows-msvc , features: feat_os_windows } + # - { os , target , cargo-options , default-features , features , dyn , use-cross , toolchain , skip-tests , workspace-tests , skip-package , skip-publish } + - { os: ubuntu-latest , target: arm-unknown-linux-gnueabihf , features: feat_os_unix_gnueabihf , use-cross: use-cross , skip-tests: true } + - { os: ubuntu-24.04-arm , target: aarch64-unknown-linux-gnu , features: feat_os_unix_gnueabihf , dyn: true } + - { os: ubuntu-latest , target: aarch64-unknown-linux-musl , features: feat_os_unix , use-cross: use-cross , skip-tests: true } + # - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: feat_selinux , use-cross: use-cross } + - { os: ubuntu-latest , target: i686-unknown-linux-gnu , features: "feat_os_unix,test_risky_names" , dyn: true , use-cross: use-cross } + - { os: ubuntu-latest , target: i686-unknown-linux-musl , features: feat_os_unix , use-cross: use-cross } + - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: "feat_os_unix,test_risky_names" , dyn: true , use-cross: use-cross } + - { os: ubuntu-latest , target: x86_64-unknown-linux-gnu , features: "feat_os_unix,uudoc" , use-cross: no, workspace-tests: true } + - { os: ubuntu-latest , target: x86_64-unknown-linux-musl , features: feat_os_unix , use-cross: use-cross } + - { os: ubuntu-latest , target: x86_64-unknown-redox , features: feat_os_unix_redox , dyn: true , use-cross: redoxer , skip-tests: true } + - { os: ubuntu-latest , target: wasm32-unknown-unknown , default-features: false, features: uucore/format, skip-tests: true, skip-package: true, skip-publish: true } + - { os: macos-latest , target: aarch64-apple-darwin , features: feat_os_macos , dyn: true , workspace-tests: true } # M1 CPU + - { os: macos-13 , target: x86_64-apple-darwin , features: feat_os_macos , dyn: true , workspace-tests: true } + - { os: windows-latest , target: i686-pc-windows-msvc , features: feat_os_windows } # TODO: Re-enable after rust-onig release: https://github.com/rust-onig/rust-onig/issues/193 # - { os: windows-latest , target: x86_64-pc-windows-gnu , features: feat_os_windows } - - { os: windows-latest , target: x86_64-pc-windows-msvc , features: feat_os_windows } - - { os: windows-latest , target: aarch64-pc-windows-msvc , features: feat_os_windows, use-cross: use-cross , skip-tests: true } + - { os: windows-latest , target: x86_64-pc-windows-msvc , features: feat_os_windows } + - { os: windows-latest , target: aarch64-pc-windows-msvc , features: feat_os_windows , use-cross: use-cross , skip-tests: true } steps: - uses: actions/checkout@v4 with: @@ -765,6 +765,23 @@ jobs: ## Build ${{ steps.vars.outputs.CARGO_CMD }} ${{ steps.vars.outputs.CARGO_CMD_OPTIONS }} build --release \ --target=${{ matrix.job.target }} ${{ matrix.job.cargo-options }} ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} ${{ steps.vars.outputs.CARGO_DEFAULT_FEATURES_OPTION }} + - name: Build dynamic library + if: matrix.job.dyn == true + shell: bash + run: | + ## Build dynamic library + ${{ steps.vars.outputs.CARGO_CMD }} ${{ steps.vars.outputs.CARGO_CMD_OPTIONS }} rustc --release --target-dir ./target/${{ matrix.job.target }}/dynamic --crate-type=dylib --lib \ + --target=${{ matrix.job.target }} ${{ matrix.job.cargo-options }} ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }},dynamic ${{ steps.vars.outputs.CARGO_DEFAULT_FEATURES_OPTION }} + ## Build the multicall shim + ${{ steps.vars.outputs.CARGO_CMD }} ${{ steps.vars.outputs.CARGO_CMD_OPTIONS }} build --release --target-dir ./target/${{ matrix.job.target }}/dynamic \ + --target=${{ matrix.job.target }} ${{ matrix.job.cargo-options }} ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }},dynamic ${{ steps.vars.outputs.CARGO_DEFAULT_FEATURES_OPTION }} + - name: Check dynamic library + if: matrix.job.dyn == true && matrix.job.skip-tests != true + shell: bash + run: | + export LD_LIBRARY_PATH="./target/${{ matrix.job.target }}/dynamic/:$LD_LIBRARY_PATH" + ${{ steps.vars.outputs.CARGO_CMD }} ${{ steps.vars.outputs.CARGO_CMD_OPTIONS }} run --release --target-dir ./target/${{ matrix.job.target }}/dynamic \ + --target=${{ matrix.job.target }} ${{ matrix.job.cargo-options }} ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }},dynamic ${{ steps.vars.outputs.CARGO_DEFAULT_FEATURES_OPTION }} - name: Test if: matrix.job.skip-tests != true shell: bash diff --git a/Cargo.lock b/Cargo.lock index 70657f90ee3..4ae5942d0b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -473,6 +473,7 @@ dependencies = [ "glob", "hex-literal", "libc", + "libloading", "nix", "num-prime", "phf", diff --git a/Cargo.toml b/Cargo.toml index a097f54ba10..abe3f6ab99f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ # coreutils (uutils) # * see the repository LICENSE, README, and CONTRIBUTING files for more information -# spell-checker:ignore (libs) bigdecimal datetime serde bincode gethostid kqueue libselinux mangen memmap procfs uuhelp startswith constness expl +# spell-checker:ignore (libs) bigdecimal datetime serde bincode gethostid kqueue libselinux libloading mangen memmap procfs uuhelp startswith constness expl [package] name = "coreutils" @@ -36,6 +36,8 @@ expensive_tests = [] test_risky_names = [] # * only build `uudoc` when `--feature uudoc` is activated uudoc = ["zip", "dep:uuhelp_parser"] +# * only build `coreutils` with dynamic loading when `--feature dynamic` is activated +dynamic = ["libloading"] ## features # "feat_acl" == enable support for ACLs (access control lists; by using`--features feat_acl`) # NOTE: @@ -304,6 +306,7 @@ iana-time-zone = "0.1.57" indicatif = "0.17.8" itertools = "0.14.0" libc = "0.2.172" +libloading = "0.8.6" linux-raw-sys = "0.9" lscolors = { version = "0.20.0", default-features = false, features = [ "gnu_legacy", @@ -377,6 +380,7 @@ phf = { workspace = true } selinux = { workspace = true, optional = true } textwrap = { workspace = true } zip = { workspace = true, optional = true } +libloading = { workspace = true, optional = true } uuhelp_parser = { optional = true, version = ">=0.0.19", path = "src/uuhelp_parser" } @@ -542,6 +546,10 @@ phf_codegen = { workspace = true } name = "coreutils" path = "src/bin/coreutils.rs" +[lib] +name = "coreutils" +path = "src/lib/coreutils.rs" + [[bin]] name = "uudoc" path = "src/bin/uudoc.rs" diff --git a/GNUmakefile b/GNUmakefile index f46126a82f5..0a23dd3c10c 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -1,7 +1,8 @@ -# spell-checker:ignore (misc) testsuite runtest findstring (targets) busytest toybox distclean pkgs nextest ; (vars/env) BINDIR BUILDDIR CARGOFLAGS DESTDIR DOCSDIR INSTALLDIR INSTALLEES MULTICALL DATAROOTDIR TESTDIR manpages +# spell-checker:ignore (misc) testsuite runtest findstring libcoreutils dylib (targets) busytest toybox distclean pkgs nextest ; (vars/env) BINDIR BUILDDIR CARGOFLAGS DESTDIR DOCSDIR INSTALLDIR INSTALLEES MULTICALL DATAROOTDIR TESTDIR manpages # Config options PROFILE ?= debug +DYNAMIC ?= n MULTICALL ?= n COMPLETIONS ?= y MANPAGES ?= y @@ -31,9 +32,11 @@ CARGOFLAGS ?= PREFIX ?= /usr/local DESTDIR ?= BINDIR ?= $(PREFIX)/bin +LIBDIR ?= $(PREFIX)/lib DATAROOTDIR ?= $(PREFIX)/share INSTALLDIR_BIN=$(DESTDIR)$(BINDIR) +INSTALLDIR_LIB=$(DESTDIR)$(LIBDIR) #prefix to apply to coreutils binary and all tool binaries PROG_PREFIX ?= @@ -271,6 +274,17 @@ TEST_SPEC_FEATURE := selinux BUILD_SPEC_FEATURE := selinux endif +ifeq (${DYNAMIC}, y) +BUILD_SPEC_FEATURE += dynamic +ifeq ($(OS),Windows_NT) +LIBNAME := libcoreutils.dll +else ifeq ($(shell uname -s),Darwin) +LIBNAME := libcoreutils.dylib +else +LIBNAME := libcoreutils.so +endif +endif + define TEST_BUSYBOX test_busybox_$(1): -(cd $(BUSYBOX_SRC)/testsuite && bindir=$(BUILDDIR) ./runtest $(RUNTEST_ARGS) $(1)) @@ -299,10 +313,15 @@ else endif endif +build-lib: +ifeq (${DYNAMIC}, y) + ${CARGO} rustc ${CARGOFLAGS} --features "${EXES} $(BUILD_SPEC_FEATURE)" ${PROFILE_CMD} --crate-type dylib --lib +endif + build-coreutils: ${CARGO} build ${CARGOFLAGS} --features "${EXES} $(BUILD_SPEC_FEATURE)" ${PROFILE_CMD} --no-default-features -build: build-coreutils build-pkgs +build: build-coreutils build-pkgs build-lib $(foreach test,$(filter-out $(SKIP_UTILS),$(PROGS)),$(eval $(call TEST_BUSYBOX,$(test)))) @@ -341,7 +360,7 @@ $(BUILDDIR)/busybox: busybox-src build-coreutils $(BUILDDIR)/.config chmod +x $@ prepare-busytest: $(BUILDDIR)/busybox - # disable inapplicable tests +# disable inapplicable tests -( cd "$(BUSYBOX_SRC)/testsuite" ; if [ -e "busybox.tests" ] ; then mv busybox.tests busybox.tests- ; fi ; ) ifeq ($(EXES),) @@ -395,7 +414,12 @@ else install-completions: endif -install: build install-manpages install-completions +install-lib: +ifeq (${DYNAMIC}, y) + $(INSTALL) $(BUILDDIR)/$(LIBNAME) $(INSTALLDIR_LIB)/ +endif + +install: build install-manpages install-completions install-lib mkdir -p $(INSTALLDIR_BIN) ifeq (${MULTICALL}, y) $(INSTALL) $(BUILDDIR)/coreutils $(INSTALLDIR_BIN)/$(PROG_PREFIX)coreutils @@ -413,6 +437,9 @@ endif uninstall: ifeq (${MULTICALL}, y) rm -f $(addprefix $(INSTALLDIR_BIN)/,$(PROG_PREFIX)coreutils) +endif +ifeq (${DYNAMIC}, y) + rm -f $(INSTALLDIR_BIN)/$(LIBNAME) endif rm -f $(addprefix $(INSTALLDIR_BIN)/$(PROG_PREFIX),$(PROGS)) rm -f $(INSTALLDIR_BIN)/$(PROG_PREFIX)[ diff --git a/README.md b/README.md index 1d9a7ddd190..e83ef8421a2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ - +
@@ -129,6 +129,22 @@ the `--package` [aka `-p`] option). For example: cargo build -p uu_base32 -p uu_cat -p uu_echo -p uu_rm ``` +The multicall code can also be built as a dynamic library, and the multicall +binary turned into a minimal shim to load it: + +```shell +cargo rustc --release --features dynamic,unix --lib --crate-type dylib +cargo build --release --features dynamic +``` + +Note that the shim multicall binary will not function unless the library is +accessible in one of the dynamic loader's search paths—either a default library +search path, or a modified environment. For example, on Linux: + +```shell +LD_LIBRARY_PATH=$(pwd)/target/release/ ./target/release/coreutils +``` + ### GNU Make Building using `make` is a simple process as well. @@ -212,13 +228,26 @@ To install the multicall binary: make MULTICALL=y install ``` -Set install parent directory (default value is /usr/local): +Set install parent directory (default value is `/usr/local`): ```shell # DESTDIR is also supported make PREFIX=/my/path install ``` +The dynamically liked multicall library and binary can be installed instead. +Note, however, that while the standard local `/usr/local/bin` path for binaries +is typically in the default `PATH` environment variable, the standard local +`/usr/local/lib` path for libraries is typically *not* in the default library +search paths. If you do not address this in a persistent manner, then do a +default dynamic install, your system will likely to fail to fully boot. With +that in mind, to install the dynamically linked library and shim multicall +binary: + +```shell +make MULTICALL=y DYNAMIC=y install +``` + Installing with `make` installs shell completions for all installed utilities for `bash`, `fish` and `zsh`. Completions for `elvish` and `powershell` can also be generated; See `Manually install shell completions`. @@ -295,6 +324,12 @@ To uninstall the multicall binary: make MULTICALL=y uninstall ``` +Similarly, for the dynamically linked library and binary: + +```shell +make MULTICALL=y DYNAMIC=y uninstall +``` + To uninstall from a custom parent directory: ```shell diff --git a/build.rs b/build.rs index 3b6aa3878d1..e0a14260631 100644 --- a/build.rs +++ b/build.rs @@ -3,7 +3,7 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (vars) krate +// spell-checker:ignore (vars) krate libloading use std::env; use std::fs::File; @@ -32,11 +32,13 @@ pub fn main() { // Allow this as we have a bunch of info in the comments #[allow(clippy::match_same_arms)] match krate.as_ref() { - "default" | "macos" | "unix" | "windows" | "selinux" | "zip" => continue, // common/standard feature names + "default" | "macos" | "unix" | "windows" | "selinux" | "zip" | "libloading" => { + continue; + } // common/standard feature names "nightly" | "test_unimplemented" | "expensive_tests" | "test_risky_names" => { continue; } // crate-local custom features - "uudoc" => continue, // is not a utility + "uudoc" | "dynamic" => continue, // is not a utility "test" => continue, // over-ridden with 'uu_test' to avoid collision with rust core crate 'test' s if s.starts_with(FEATURE_PREFIX) => continue, // crate feature sets _ => {} // util feature name diff --git a/src/bin/coreutils.rs b/src/bin/coreutils.rs index b29e7ea2337..45c23685c39 100644 --- a/src/bin/coreutils.rs +++ b/src/bin/coreutils.rs @@ -3,239 +3,22 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore manpages mangen +// spell-checker:ignore libcoreutils libloading -use clap::{Arg, Command}; -use clap_complete::Shell; -use std::cmp; -use std::ffi::OsStr; -use std::ffi::OsString; -use std::io::{self, Write}; -use std::path::{Path, PathBuf}; -use std::process; -use uucore::display::Quotable; - -const VERSION: &str = env!("CARGO_PKG_VERSION"); - -include!(concat!(env!("OUT_DIR"), "/uutils_map.rs")); - -fn usage(utils: &UtilityMap, name: &str) { - println!("{name} {VERSION} (multi-call binary)\n"); - println!("Usage: {name} [function [arguments...]]"); - println!(" {name} --list\n"); - println!("Options:"); - println!(" --list lists all defined functions, one per row\n"); - println!("Currently defined functions:\n"); - #[allow(clippy::map_clone)] - let mut utils: Vec<&str> = utils.keys().map(|&s| s).collect(); - utils.sort_unstable(); - let display_list = utils.join(", "); - let width = cmp::min(textwrap::termwidth(), 100) - 4 * 2; // (opinion/heuristic) max 100 chars wide with 4 character side indentions - println!( - "{}", - textwrap::indent(&textwrap::fill(&display_list, width), " ") - ); -} - -/// # Panics -/// Panics if the binary path cannot be determined -fn binary_path(args: &mut impl Iterator) -> PathBuf { - match args.next() { - Some(ref s) if !s.is_empty() => PathBuf::from(s), - _ => std::env::current_exe().unwrap(), - } -} - -fn name(binary_path: &Path) -> Option<&str> { - binary_path.file_stem()?.to_str() -} - -#[allow(clippy::cognitive_complexity)] +#[cfg(not(feature = "dynamic"))] fn main() { - uucore::panic::mute_sigpipe_panic(); - - let utils = util_map(); - let mut args = uucore::args_os(); - - let binary = binary_path(&mut args); - let binary_as_util = name(&binary).unwrap_or_else(|| { - usage(&utils, ""); - process::exit(0); - }); - - // binary name equals util name? - if let Some(&(uumain, _)) = utils.get(binary_as_util) { - process::exit(uumain(vec![binary.into()].into_iter().chain(args))); - } - - // binary name equals prefixed util name? - // * prefix/stem may be any string ending in a non-alphanumeric character - let util_name = if let Some(util) = utils.keys().find(|util| { - binary_as_util.ends_with(*util) - && !binary_as_util[..binary_as_util.len() - (*util).len()] - .ends_with(char::is_alphanumeric) - }) { - // prefixed util => replace 0th (aka, executable name) argument - Some(OsString::from(*util)) - } else { - // unmatched binary name => regard as multi-binary container and advance argument list - uucore::set_utility_is_second_arg(); - args.next() - }; - - // 0th argument equals util name? - if let Some(util_os) = util_name { - fn not_found(util: &OsStr) -> ! { - println!("{}: function/utility not found", util.maybe_quote()); - process::exit(1); - } - - let Some(util) = util_os.to_str() else { - not_found(&util_os) - }; - - match util { - "completion" => gen_completions(args, &utils), - "manpage" => gen_manpage(args, &utils), - "--list" => { - let mut utils: Vec<_> = utils.keys().collect(); - utils.sort(); - for util in utils { - println!("{util}"); - } - process::exit(0); - } - // Not a special command: fallthrough to calling a util - _ => {} - } - - match utils.get(util) { - Some(&(uumain, _)) => { - process::exit(uumain(vec![util_os].into_iter().chain(args))); - } - None => { - if util == "--help" || util == "-h" { - // see if they want help on a specific util - if let Some(util_os) = args.next() { - let Some(util) = util_os.to_str() else { - not_found(&util_os) - }; - - match utils.get(util) { - Some(&(uumain, _)) => { - let code = uumain( - vec![util_os, OsString::from("--help")] - .into_iter() - .chain(args), - ); - io::stdout().flush().expect("could not flush stdout"); - process::exit(code); - } - None => not_found(&util_os), - } - } - usage(&utils, binary_as_util); - process::exit(0); - } else { - not_found(&util_os); - } - } - } - } else { - // no arguments provided - usage(&utils, binary_as_util); - process::exit(0); - } + coreutils::multicall_main(); } -/// Prints completions for the utility in the first parameter for the shell in the second parameter to stdout -/// # Panics -/// Panics if the utility map is empty -fn gen_completions( - args: impl Iterator, - util_map: &UtilityMap, -) -> ! { - let all_utilities: Vec<_> = std::iter::once("coreutils") - .chain(util_map.keys().copied()) - .collect(); - - let matches = Command::new("completion") - .about("Prints completions to stdout") - .arg( - Arg::new("utility") - .value_parser(clap::builder::PossibleValuesParser::new(all_utilities)) - .required(true), - ) - .arg( - Arg::new("shell") - .value_parser(clap::builder::EnumValueParser::::new()) - .required(true), - ) - .get_matches_from(std::iter::once(OsString::from("completion")).chain(args)); - - let utility = matches.get_one::("utility").unwrap(); - let shell = *matches.get_one::("shell").unwrap(); - - let mut command = if utility == "coreutils" { - gen_coreutils_app(util_map) - } else { - util_map.get(utility).unwrap().1() - }; - let bin_name = std::env::var("PROG_PREFIX").unwrap_or_default() + utility; - - clap_complete::generate(shell, &mut command, bin_name, &mut io::stdout()); - io::stdout().flush().unwrap(); - process::exit(0); -} - -/// Generate the manpage for the utility in the first parameter -/// # Panics -/// Panics if the utility map is empty -fn gen_manpage( - args: impl Iterator, - util_map: &UtilityMap, -) -> ! { - let all_utilities: Vec<_> = std::iter::once("coreutils") - .chain(util_map.keys().copied()) - .collect(); - - let matches = Command::new("manpage") - .about("Prints manpage to stdout") - .arg( - Arg::new("utility") - .value_parser(clap::builder::PossibleValuesParser::new(all_utilities)) - .required(true), - ) - .get_matches_from(std::iter::once(OsString::from("manpage")).chain(args)); - - let utility = matches.get_one::("utility").unwrap(); - - let command = if utility == "coreutils" { - gen_coreutils_app(util_map) - } else { - util_map.get(utility).unwrap().1() - }; - - let man = clap_mangen::Man::new(command); - man.render(&mut io::stdout()) - .expect("Man page generation failed"); - io::stdout().flush().unwrap(); - process::exit(0); -} - -/// # Panics -/// Panics if the utility map is empty -fn gen_coreutils_app(util_map: &UtilityMap) -> Command { - let mut command = Command::new("coreutils"); - for (name, (_, sub_app)) in util_map { - // Recreate a small subcommand with only the relevant info - // (name & short description) - let about = sub_app() - .get_about() - .expect("Could not get the 'about'") - .to_string(); - let sub_app = Command::new(name).about(about); - command = command.subcommand(sub_app); +#[cfg(feature = "dynamic")] +fn main() { + use libloading::{Library, Symbol, library_filename}; + unsafe { + let library = Library::new(library_filename("coreutils")) + .unwrap_or_else(|e| panic!("Could not load libcoreutils: {}", e)); + let library_main: Symbol = library + .get(b"coreutils_multicall_main_wrapper") + .unwrap_or_else(|e| panic!("Could not find main symbol: {}", e)); + library_main(); } - command } diff --git a/src/lib/coreutils.rs b/src/lib/coreutils.rs new file mode 100644 index 00000000000..68da02af11a --- /dev/null +++ b/src/lib/coreutils.rs @@ -0,0 +1,251 @@ +// This file is part of the uutils coreutils package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +// spell-checker:ignore manpages mangen + +use clap::{Arg, Command}; +use clap_complete::Shell; +use std::cmp; +use std::ffi::OsStr; +use std::ffi::OsString; +use std::io::{self, Write}; +use std::path::{Path, PathBuf}; +use std::process; +use uucore::display::Quotable; + +const VERSION: &str = env!("CARGO_PKG_VERSION"); + +include!(concat!(env!("OUT_DIR"), "/uutils_map.rs")); + +fn usage(utils: &UtilityMap, name: &str) { + #[cfg(not(feature = "dynamic"))] + let kind = "binary"; + #[cfg(feature = "dynamic")] + let kind = "dynamic library"; + println!("{name} {VERSION} (multi-call {kind})\n"); + println!("Usage: {name} [function [arguments...]]"); + println!(" {name} --list\n"); + println!("Options:"); + println!(" --list lists all defined functions, one per row\n"); + println!("Currently defined functions:\n"); + #[allow(clippy::map_clone)] + let mut utils: Vec<&str> = utils.keys().map(|&s| s).collect(); + utils.sort_unstable(); + let display_list = utils.join(", "); + let width = cmp::min(textwrap::termwidth(), 100) - 4 * 2; // (opinion/heuristic) max 100 chars wide with 4 character side indentions + println!( + "{}", + textwrap::indent(&textwrap::fill(&display_list, width), " ") + ); +} + +/// # Panics +/// Panics if the binary path cannot be determined +fn binary_path(args: &mut impl Iterator) -> PathBuf { + match args.next() { + Some(ref s) if !s.is_empty() => PathBuf::from(s), + _ => std::env::current_exe().unwrap(), + } +} + +fn name(binary_path: &Path) -> Option<&str> { + binary_path.file_stem()?.to_str() +} + +#[allow(clippy::cognitive_complexity)] +pub fn multicall_main() { + uucore::panic::mute_sigpipe_panic(); + + let utils = util_map(); + let mut args = uucore::args_os(); + + let binary = binary_path(&mut args); + let binary_as_util = name(&binary).unwrap_or_else(|| { + usage(&utils, ""); + process::exit(0); + }); + + // binary name equals util name? + if let Some(&(uumain, _)) = utils.get(binary_as_util) { + process::exit(uumain(vec![binary.into()].into_iter().chain(args))); + } + + // binary name equals prefixed util name? + // * prefix/stem may be any string ending in a non-alphanumeric character + let util_name = if let Some(util) = utils.keys().find(|util| { + binary_as_util.ends_with(*util) + && !binary_as_util[..binary_as_util.len() - (*util).len()] + .ends_with(char::is_alphanumeric) + }) { + // prefixed util => replace 0th (aka, executable name) argument + Some(OsString::from(*util)) + } else { + // unmatched binary name => regard as multi-binary container and advance argument list + uucore::set_utility_is_second_arg(); + args.next() + }; + + // 0th argument equals util name? + if let Some(util_os) = util_name { + fn not_found(util: &OsStr) -> ! { + println!("{}: function/utility not found", util.maybe_quote()); + process::exit(1); + } + + let Some(util) = util_os.to_str() else { + not_found(&util_os) + }; + + match util { + "completion" => gen_completions(args, &utils), + "manpage" => gen_manpage(args, &utils), + "--list" => { + let mut utils: Vec<_> = utils.keys().collect(); + utils.sort(); + for util in utils { + println!("{util}"); + } + process::exit(0); + } + // Not a special command: fallthrough to calling a util + _ => {} + } + + match utils.get(util) { + Some(&(uumain, _)) => { + process::exit(uumain(vec![util_os].into_iter().chain(args))); + } + None => { + if util == "--help" || util == "-h" { + // see if they want help on a specific util + if let Some(util_os) = args.next() { + let Some(util) = util_os.to_str() else { + not_found(&util_os) + }; + + match utils.get(util) { + Some(&(uumain, _)) => { + let code = uumain( + vec![util_os, OsString::from("--help")] + .into_iter() + .chain(args), + ); + io::stdout().flush().expect("could not flush stdout"); + process::exit(code); + } + None => not_found(&util_os), + } + } + usage(&utils, binary_as_util); + process::exit(0); + } else { + not_found(&util_os); + } + } + } + } else { + // no arguments provided + usage(&utils, binary_as_util); + process::exit(0); + } +} + +/// Prints completions for the utility in the first parameter for the shell in the second parameter to stdout +/// # Panics +/// Panics if the utility map is empty +fn gen_completions( + args: impl Iterator, + util_map: &UtilityMap, +) -> ! { + let all_utilities: Vec<_> = std::iter::once("coreutils") + .chain(util_map.keys().copied()) + .collect(); + + let matches = Command::new("completion") + .about("Prints completions to stdout") + .arg( + Arg::new("utility") + .value_parser(clap::builder::PossibleValuesParser::new(all_utilities)) + .required(true), + ) + .arg( + Arg::new("shell") + .value_parser(clap::builder::EnumValueParser::::new()) + .required(true), + ) + .get_matches_from(std::iter::once(OsString::from("completion")).chain(args)); + + let utility = matches.get_one::("utility").unwrap(); + let shell = *matches.get_one::("shell").unwrap(); + + let mut command = if utility == "coreutils" { + gen_coreutils_app(util_map) + } else { + util_map.get(utility).unwrap().1() + }; + let bin_name = std::env::var("PROG_PREFIX").unwrap_or_default() + utility; + + clap_complete::generate(shell, &mut command, bin_name, &mut io::stdout()); + io::stdout().flush().unwrap(); + process::exit(0); +} + +/// Generate the manpage for the utility in the first parameter +/// # Panics +/// Panics if the utility map is empty +fn gen_manpage( + args: impl Iterator, + util_map: &UtilityMap, +) -> ! { + let all_utilities: Vec<_> = std::iter::once("coreutils") + .chain(util_map.keys().copied()) + .collect(); + + let matches = Command::new("manpage") + .about("Prints manpage to stdout") + .arg( + Arg::new("utility") + .value_parser(clap::builder::PossibleValuesParser::new(all_utilities)) + .required(true), + ) + .get_matches_from(std::iter::once(OsString::from("manpage")).chain(args)); + + let utility = matches.get_one::("utility").unwrap(); + + let command = if utility == "coreutils" { + gen_coreutils_app(util_map) + } else { + util_map.get(utility).unwrap().1() + }; + + let man = clap_mangen::Man::new(command); + man.render(&mut io::stdout()) + .expect("Man page generation failed"); + io::stdout().flush().unwrap(); + process::exit(0); +} + +/// # Panics +/// Panics if the utility map is empty +fn gen_coreutils_app(util_map: &UtilityMap) -> Command { + let mut command = Command::new("coreutils"); + for (name, (_, sub_app)) in util_map { + // Recreate a small subcommand with only the relevant info + // (name & short description) + let about = sub_app() + .get_about() + .expect("Could not get the 'about'") + .to_string(); + let sub_app = Command::new(name).about(about); + command = command.subcommand(sub_app); + } + command +} + +#[cfg(feature = "dynamic")] +#[unsafe(no_mangle)] +pub extern "Rust" fn coreutils_multicall_main_wrapper() { + multicall_main(); +}