diff --git a/.gitignore b/.gitignore index d1338731..c53cd369 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ bin Cargo.lock .* *.swp +rustc-ice-*.txt diff --git a/Cargo.toml b/Cargo.toml index a2b2d6f7..4d0b7dfa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ncurses" -version = "6.0.0" +version = "6.0.1" authors = [ "contact@jeaye.com" ] description = "A very thin wrapper around the ncurses TUI library" documentation = "https://github.com/jeaye/ncurses-rs" @@ -9,10 +9,27 @@ repository = "https://github.com/jeaye/ncurses-rs" readme = "README.md" keywords = ["ncurses","TUI"] license = "MIT" +#If the edition field is not present in Cargo.toml, then the 2015 edition is assumed for backwards compatibility. +#Set to avoid a warning with newer rust: +#The Rust 2021 Edition was introduced in Rust 1.56.0. +edition = "2021" build = "build.rs" +#https://doc.rust-lang.org/cargo/reference/manifest.html#the-rust-version-field +rust-version = "1.57.0" +#Using this key rust-version= here aka MSRV, makes 1.56.0 be the minimum supported rust version(MSRV) +#The first version of Cargo that supports this field was released with Rust 1.56.0. In older releases, the field will be ignored, and Cargo will display a warning. +#Due to addr_of!() MSRV 1.51.0 is required minimum. +#cc 1.0.92 requires 1.53.0 MSRV(but its `cargo test` passes only with 1.63.0), +# however `cargo build` will pull latest cc due to Cargo.lock missing(on first repo clone) +# and cc version specified isn't fixed like cc="=1.0.92", so: +# due to latest cc pulled being 1.0.95(21Apr2024) it requires 1.63.0 MSRV (ie: `cargo msrv list`) +#Due to build.rs' use of std::process::Command::get_*() MSRV is 1.57.0 +#To minimize MSRV further you can set `cc = "=1.0.92"` (note the extra "=") below then run `cargo update` +# that makes MSRV be 1.53.0 minimum, but other things will raise it to 1.57.0 +#MSRV is 1.56.0 due to the above edition="2021" [build-dependencies] -cc = "1.0.18" +cc = "1.0.92" pkg-config = "0.3" [dependencies] diff --git a/build.rs b/build.rs index 1489d532..78a65343 100644 --- a/build.rs +++ b/build.rs @@ -1,173 +1,1290 @@ +#![allow(clippy::uninlined_format_args)] // or is it more readable inlined? + +#[cfg(target_os = "windows")] +compile_error!("You can't compile this ncurses crate on native Windows, but you could compile it on Windows Subsystem for Linux aka WSL because that's seen as unix. If you want native Windows then consider using pancurses instead, because it uses pdcurses instead."); + extern crate cc; extern crate pkg_config; use pkg_config::Library; use std::env; +use std::ffi::OsStr; +use std::fmt; use std::fs::File; -use std::io::Write; +use std::io::Write as required_for_write_all_function; //in File +#[cfg(not(target_os = "windows"))] +use std::os::unix::ffi::OsStrExt; use std::path::Path; use std::process::Command; +use std::process::ExitStatus; + +//Decide whether or not to delete .c and bin files generated by build.rs once they're not needed. +//Defaulting to 'false' because it's a job for 'cargo clean' and +//it might help with debugging build issues if we keep them around. +//Even if this were true, we're already keeping 'libwrap.a' which we can't delete because cargo needs. +const DELETE_GENERATEDS: bool = false; + +// Optional environment variables: + +// The below doc comment doesn't apply for these 2 env.vars: +const ENV_VAR_NAME_FOR_LIB: &str = "NCURSES_RS_RUSTC_LINK_LIB"; +const ENV_VAR_NAME_FOR_NCURSES_RS_RUSTC_FLAGS: &str = "NCURSES_RS_RUSTC_FLAGS"; + +/// Assuming we want env.var "NCURSES_RS_CFLAGS" here, +/// and target==host and is "x86_64-unknown-linux-gnu" +/// then calls to `Build::try_flags_from_environment()` below in code, +/// will try the following env.vars in this order: +/// 1. "NCURSES_RS_CFLAGS_x86_64-unknown-linux-gnu" (notice dashes) +/// 2. "NCURSES_RS_CFLAGS_x86_64_unknown_linux_gnu" (notice underscores) +/// 3. "HOST_NCURSES_RS_CFLAGS" or "TARGET_NCURSES_RS_CFLAGS" (if target!=host) +/// 4. "NCURSES_RS_CFLAGS" (our original wanted) +/// and the first one that exists is used instead. +/// see: https://docs.rs/cc/1.0.92/src/cc/lib.rs.html#3571-3580 +/// All of the _tried_ ones are emitted as: cargo:rerun-if-env-changed= +/// which means, if NCURSES_RS_CFLAGS_x86_64_unknown_linux_gnu is set then NCURSES_RS_CFLAGS won't +/// be emitted which makes sense as this one overrides the rest anyway. +const ENV_VAR_NAME_FOR_NCURSES_RS_CFLAGS: &str = "NCURSES_RS_CFLAGS"; +const IS_WIDE: bool = cfg!(feature = "wide"); +const IS_MACOS: bool = cfg!(target_os = "macos"); +// Why _also_ not on macos? see: https://github.com/jeaye/ncurses-rs/issues/151 +const IS_WIDE_AND_NOT_ON_MACOS: bool = IS_WIDE && !IS_MACOS; + +const NCURSES_LIB_NAME_FALLBACK: &str = if IS_WIDE_AND_NOT_ON_MACOS { + "ncursesw" +} else { + "ncurses" +}; +// Will search for these lib names and if not found via pkg-config +// then use the fallback name and still try linking with it +// because in most cases it will work anyway. +const NCURSES_LIB_NAMES: &[&str] = if IS_WIDE_AND_NOT_ON_MACOS { + &["ncursesw5", NCURSES_LIB_NAME_FALLBACK] +} else { + &["ncurses5", NCURSES_LIB_NAME_FALLBACK] +}; + +const MENU_LIB_NAME_FALLBACK: &str = if IS_WIDE_AND_NOT_ON_MACOS { + "menuw" +} else { + "menu" +}; +const MENU_LIB_NAMES: &[&str] = if IS_WIDE_AND_NOT_ON_MACOS { + &["menuw5", MENU_LIB_NAME_FALLBACK] +} else { + &["menu5", MENU_LIB_NAME_FALLBACK] +}; + +const PANEL_LIB_NAME_FALLBACK: &str = if IS_WIDE_AND_NOT_ON_MACOS { + "panelw" +} else { + "panel" +}; +const PANEL_LIB_NAMES: &[&str] = if IS_WIDE_AND_NOT_ON_MACOS { + &["panelw5", PANEL_LIB_NAME_FALLBACK] +} else { + &["panel5", PANEL_LIB_NAME_FALLBACK] +}; + +const TINFO_LIB_NAMES: &[&str] = if IS_WIDE_AND_NOT_ON_MACOS { + //elements order here matters, because: + //Fedora/Ubuntu has ncursesw+tinfo(without w) for wide! + //and -ltinfow fails to link on NixOS and Fedora! so -ltinfo must be used even tho wide. + //(presumably because tinfo doesn't depend on wideness?) + //NixOS has only ncursesw(tinfo is presumably inside it) but -ltinfo still works for it(it's a + //symlink to ncursesw lib) + //Gentoo has ncursesw+tinfow + // + //These are tried in order and first that links is selected: + &["tinfow5", "tinfow", "tinfo"] + //doneFIXME: here ^, user can have in env. this TINFOW_NO_PKG_CONFIG=1 (but not also TINFO_NO_PKG_CONFIG=1) which would cause seg fault on Gentoo because tinfo will be found&linked(instead of tinfow) with one or more of menuw,panelw,ncursesw eg. when doing example ex_5 (ie. menuw,ncursesw,tinfo(no w)); but on Fedora this ncursesw+tinfo(no w) makes sense(because tinfo(no w) has both inside it, somehow, i guess), so we can't really guard against this (well maybe with target_os but what if they change in the future...) instead maybe print a warning if w and non-w are mixed(but only for tinfo is needed), even though it will be a false warning on Fedora, well maybe it won't be if we also check if env. var is set TINFOW_NO_PKG_CONFIG. +} else { + //no reason to ever fallback to tinfow here when not-wide! + //Fedora/Gentoo has ncurses+tinfo + //NixOS has only ncursesw(but works for non-wide), -ltinfo symlinks to ncursesw .so file) + //so 'tinfo' is safe fallback here. + &["tinfo5", "tinfo"] +}; +//TODO: why are we trying the v5 of the lib first instead of v6 (which is the second/last in list), +//was v5 newer than the next in list? is it so on other systems? +//like: was it ever ncurses5 newer than ncurses ? +//Since we're trying v5 and it finds it, it will use it and stop looking, +//even though the next one in list might be v6 +//This is the commit that added this v5 then v6 way: https://github.com/jeaye/ncurses-rs/commit/daddcbb557169cfac03af9667ef7aefed19f9409 + +/// Uses pkg-config to find the lib and then emits cargo:rustc-link-lib= for it +/// eg. `pkg-config ncurses --libs` +/// link searchdirs and header include dirs, if any, are also in the result. fn find_library(names: &[&str]) -> Option { for name in names { + //cargo_warn!("Trying lib '{}'",name); if let Ok(lib) = pkg_config::probe_library(name) { + //cargo_warn!("Found lib '{}' '{:?}'",name, lib); return Some(lib); } } None } +/// Emits the passed string(s) prefixed by 'cargo:warning=' on stdout, +/// which cargo will transform into a warning. +/// It acts like println!() macro, so you can call it the same way to do formatting! +/// Will replace newlines in the warning message with spaces, +/// otherwise the text after would not have been seen in the warning. +macro_rules! cargo_warn { + ($($arg:tt)*) => { + cargo_warn_unformatted(&format!("{}", format_args!($($arg)*))); + }; +} + +/// Pass the string to be emitted as a cargo warning. +/// Presumably you've already used format!() on it. +/// Will replace newlines in the warning message with spaces, +/// otherwise the text after would not have been seen in the warning. +fn cargo_warn_unformatted(warn_msg: &str) { + // Replace '\r' with nothing + // Replace '\n' with space + let warn_msg = warn_msg.replace('\r', "").replace('\n', " "); + println!("cargo:warning={}", warn_msg); +} +// ----------------------------------------------------------------- +// This is the normal build.rs main(), fn main() { - println!("cargo:rerun-if-env-changed=PKG_CONFIG_PATH"); + watch_env_var("PKG_CONFIG_PATH"); - let wide = cfg!(all(feature = "wide", not(target_os = "macos"))); + //This is what pkg-config finds, if any, including link searchdirs and include dirs for it! + let ncurses_lib = find_library(NCURSES_LIB_NAMES); + // Gets the name of ncurses lib found by pkg-config, if it found any! + // else (warns and)returns the default one like 'ncurses' or 'ncursesw' + // and emits cargo:rustc-link-lib= for it unless already done. + let lib_name = get_ncurses_lib_name(&ncurses_lib); + //XXX: cargo seems to use --as-needed (arg for 'ld' linker) which isn't easy to figure out(had + //to replace /usr/bin/ld temporarily, it won't work with just setting PATH because cc calls + //sibbling ld disregarding PATH), but this means the order in which + //we try to find the libs here matters(in theory): now it's ncurses,menu,panel,tinfo + //Not entirely sure here if this will break things in practice(our case), as we previously used: + //menu,panel,tinfo,ncurses order + // https://wiki.gentoo.org/wiki/Project:Quality_Assurance/As-needed#Importance_of_linking_order + // So in theory(untested), if a bin linked with cargo and only used symbols from menu not from ncurses, + // and since menu wanted symbols from ncurses (like it does on OpenBSD), due to + // -Wl,--as-needed, ncurses lib won't be linked in final bin(because bin doesn't use symbols + // from it) thus cause the bin to fail with unresolved symbols at runtime. + // However, in practice, we're "lucky" that anything(presumably), like any bin we make that uses our + // ncurses rust crate, will also use ncurses lib thus forcing it to be linked, + // thus menu or panel libs that require it will always have it (dyn)linked in the bin. (use ldd on it to see) + // An unrelated note: on OpenBSD 7.5 linking with -lncursesw links with `/usr/lib/libcurses.so.15.0` which is a hardlink to `/usr/lib/libncursesw.so.15.0`, same happens for -lcurses and -lncurses, they link to the same lib, as if both normal and wide char should support should already be inside it(and they are). Same holds for libmenu(w).so.7.0 and libpanel(w).so.7.0 - let ncurses_lib_names = if wide { - &["ncursesw5", "ncursesw"] - } else { - &["ncurses5", "ncurses"] - }; - let ncurses_lib = find_library(ncurses_lib_names); + if IS_WIDE { + // A binary that uses ncurses with wide feature, is affected by the value of the following env. vars: + // "LC_ALL", "LC_CTYPE", "LANG", they should be set to eg. en_US.UTF-8 + // info from: https://invisible-island.net/ncurses/ncurses-openbsd.html#problem_locales + // and from: https://github.com/mimblewimble/grin/issues/3776#issuecomment-1826805985 - if cfg!(feature = "menu") { - if wide { - find_library(&["menuw5", "menuw"]); + // Every other distro already has at least LANG set, but is otherwise affected the same if + // for example LANG is unset or LANG=C is used, when running the binary with wide feature. + + //TODO: maybe need a better way to detect if it's UTF-8 capable and correctly set. + //Perhaps some locale crate. + + //This is the order of precedence, if any/all are set, the one leftmost in this list overrides + //the others that come after it; + let env_vars = vec!["LC_ALL", "LC_CTYPE", "LANG"]; + for var in &env_vars { + //tell cargo to rebuild if they change, else we see same warning on every `cargo run` + //even though it doesn't(presumably) affect the build, but only the bin at runtime. + watch_env_var(var); + } + //the first one(from the above) that's set, overrides the rest, so ignore the rest. + let mut first_one_set: Option<(bool, &str, String)> = None; + + /// Function to check if a string ends with the substring "UTF-8" + /// case sensitive(true) or insensitive(false) is selected by bool arg. + fn ends_in_utf8(value: &str, case_sensitive: bool) -> bool { + let ci: String = if case_sensitive { + value.to_string() + } else { + value.to_uppercase() + }; + //Gentoo can take both utf8 and utf-8, case insesitive + //but the en_US part is case sensitive! + //OpenBSD 7.5 can only take uppercase UTF-8 + //but the en_US part is case INsensitive! + ci.ends_with("UTF-8") || ci.ends_with("UTF8") + } + + for var in &env_vars { + if let Ok(value) = env::var(var) { + //we make sure to warn on openbsd if we don't find UTF-8 exactly! + //on others, we just case insensitively check if it ends with utf-8/utf8 + const CASE_SENSITIVE: bool = cfg!(target_os = "openbsd"); + if ends_in_utf8(&value, CASE_SENSITIVE) { + first_one_set = Some((true, var, value)); + } else { + first_one_set = Some((false, var, value)); + } + + break; + } + } + let extra=" Note that, at least on OpenBSD 7.5, you must set the 'UTF-8' part to be all uppercase as it seems to be case sensitive so uTf-8, Utf-8, utf-8 or even UTF8 without dash, won't work but the 'en_US' part can be any text or even empty, apparently. On Gentoo however, even utf8 without a dash works(as also with a dash like uTf-8) and it's case insensitive, however the en_US part is case sensitive! Check your distro for the correct form maybe."; + if let Some((is_utf8, var_name, var_value)) = first_one_set { + if !is_utf8 { + cargo_warn!("You've enabled the 'wide' feature but you've set environment variable '{}={}', which isn't UTF-8, apparently, so wide characters will look garbled when you run your resulting ncurses-using binary unless you set one of these environment vars {:?} to, for example, \"en_US.UTF-8\". This affects the binary at runtime not at compile time.{}",var_name,var_value, &env_vars, extra); + } + //if it is utf8, then we're all good } else { - find_library(&["menu5", "menu"]); + // no env.vars are set, is this OpenBSD?! applies to any OS though. + cargo_warn!("You've enabled the 'wide' feature but you've not set any environment variables from the set of these {:?}, so unless you set one of them to for example \"en_US.UTF-8\" then wide characters will look garbled when you run your resulting ncurses-using binary. This affects the binary at runtime not at compile time.{}", &env_vars, extra); } + } // is wide + + if cfg!(feature = "menu") { + find_sublib( + "menu", + MENU_LIB_NAMES, + MENU_LIB_NAME_FALLBACK, + &ncurses_lib, + &lib_name, + ); } if cfg!(feature = "panel") { - if wide { - find_library(&["panelw5", "panelw"]); - } else { - find_library(&["panel5", "panel"]); - } + find_sublib( + "panel", + PANEL_LIB_NAMES, + PANEL_LIB_NAME_FALLBACK, + &ncurses_lib, + &lib_name, + ); } - match std::env::var("NCURSES_RS_RUSTC_LINK_LIB") { - Ok(x) => println!("cargo:rustc-link-lib={}", x), - _ => if ncurses_lib.is_none() { - println!("cargo:rustc-link-lib={}", ncurses_lib_names.last().unwrap()) - } + //This comment block is about libtinfo. + //If pkg-config can't find it, use fallback: 'tinfo' or 'tinfow' + //If we tell cargo about it via cargo:rustc-link-lib=tinfo, + //and it can find it, it will link it, else it will err. + //It's needed for ex_5 to can link when pkg-config is missing, + //otherwise you get this: undefined reference to symbol 'noraw' + //Thus w/o this block, the following command would be needed to run ex_5 + //$ NCURSES_RS_RUSTC_FLAGS="-ltinfo" cargo run --features=menu --example ex_5 + //To emulate this even if you have pkg-config you can tell it to not do its job + // by setting these env. vars before the above command: + // $ NCURSES_NO_PKG_CONFIG=1 NCURSESW_NO_PKG_CONFIG=1 NCURSES5_NO_PKG_CONFIG=1 NCURSESW5_NO_PKG_CONFIG=1 the_rest_of_the_command_here + // Fedora and Gentoo are two that have both ncurses(w) and tinfo(w), ie. split, + // however Gentoo has ncurses+tinfo and ncursesw+tinfow, + // but Fedora&Ubuntu has ncurses+tinfo and ncursesw+tinfo (see 'tinfo' is same! no w) + // NixOS has only ncursesw (tinfo is presumably inside?) but -lncurses -lncursesw -ltinfo work! + // but -ltinfow fails on NixOS and Fedora! + // On Gentoo -ltinfow works too(ie. in addition to the other 3 variants)! + // So when pkg-config is missing, how do we know which tinfo to tell cargo to link, if any! + // we use try_link() which tries to link a dummy .c file with each tinfo variant and pick + // the first that links without errors. + let tinfo_name = if let Some(found) = find_library(TINFO_LIB_NAMES) { + let libs = found.libs; + assert_eq!( + libs.len(), + 1, + "Unexpected pkg-config query for tinfo lib returned more than one lib: '{:?}'", + libs + ); + libs.first() + .unwrap_or_else(|| { + panic!( + "Unexpected panic on trying to get the first found tinfo lib string from: '{:?}'.", + libs + ) + }) + .clone() + } else { + //None found; but at least on NixOS it works without any tinfo(it's inside ncursesw lib + //and tinfo/ncurses all symlink to that same libncursesw.so, except tinfow, + //which doesn't exist but pkg-config points it to -lncursesw), + //so no need to warn that we didn't find any tinfo. + // + //Pick the tinfo lib to link with, as fallback, + //the first one that links successfully! + //The order in the list matters! + (*TINFO_LIB_NAMES + .iter() + .find(|&each| { + let ret: bool = if let Some(needed_ncurses)=try_link(each, &ncurses_lib, &lib_name) { + let extra:String=if needed_ncurses { + format!(", but needed '{}' to link without undefined symbols", lib_name) + } else { + String::new() + }; + cargo_warn!("Using lib fallback '{}' which links successfully{}. The need for fallback suggests that you might be missing `pkg-config`/`pkgconf`.", each, extra); + println!("cargo:rustc-link-lib={}", each); + true + } else { false }; + ret + }) + .unwrap_or(&"")) // found no tinfo that links without errors which may be ok(eg. on Nixos) + .to_string() + }; + if IS_WIDE_AND_NOT_ON_MACOS + && tinfo_name == "tinfo" + && std::env::var("TINFOW_NO_PKG_CONFIG").is_ok() + { + cargo_warn!("Looks like you're using wide(and are not on macos) and you've set TINFOW_NO_PKG_CONFIG but have NOT set TINFO_NO_PKG_CONFIG too, so you're linking tinfo(no w) with other wide libs like ncursesw, which will cause '{}' eg. for example ex_5 when trying to run it. This is a warning not a panic because we assume you know what you're doing, and besides this works on Fedora (even if that env. var isn't set)!","Segmentation fault (core dumped)"); } + //done1TODO: test on macos-es. When not using the brew ncurses, it won't have A_ITALIC and BUTTON5_* + //thus cursive will fail compilation. donedifferentlyTODO: detect this and issue cargo:warning from here. - if let Ok(x) = std::env::var("NCURSES_RS_RUSTC_FLAGS") { + watch_env_var(ENV_VAR_NAME_FOR_NCURSES_RS_RUSTC_FLAGS); + if let Ok(x) = std::env::var(ENV_VAR_NAME_FOR_NCURSES_RS_RUSTC_FLAGS) { println!("cargo:rustc-flags={}", x); } check_chtype_size(&ncurses_lib); - gen_constants(); - gen_menu_constants(); - build_wrap(); + //The code in src/genconstants.c uses initscr() of ncurses (see: $ man 3x initscr) + //which depends on TERM env.var and will fail if TERM is wrong, say 'TERM=foo', + //with: "Error opening terminal: foo." and exit code 1 + //therefore rebuild if TERM is changed without needing a 'cargo clean' first: + watch_env_var("TERM"); + gen_rs( + "src/genconstants.c", + "genconstants", + "raw_constants.rs", + &ncurses_lib, + &lib_name, + "!! Maybe you need to try a different value for the TERM environment variable !!", + ); + + gen_rs( + "src/menu/genconstants.c", + "genmenuconstants", + "menu_constants.rs", + &ncurses_lib, + &lib_name, + "", + ); + + build_wrap(&ncurses_lib); } +// ----------------------------------------------------------------- -fn build_wrap() { - println!("cargo:rerun-if-changed=src/wrap.c"); - cc::Build::new() - .file("src/wrap.c") - .compile("wrap"); +/// This is for menu and panel to see if pkg-config finds them and if not then see +/// which fallback lib name links successfully and whether or not also needs ncurses(w) +/// to successfully link. +fn find_sublib( + feature_name: &str, + sublibs_list: &[&str], + sublib_fallback_name: &str, + ncurses_lib: &Option, + ncurses_lib_name_to_use: &str, +) { + if find_library(sublibs_list).is_none() { + //On openbsd(at least), the 'menu' lib linking via try_link() (ie. just -lmenu) + //fails because it depends on ncurses also being linked in (via -lncurses) + //because 'menu' lib doesn't have ncurses as a dynamic lib need, as it does on other OS-es like Gentoo + //which you can see via readelf -d /usr/lib/libmenu.so.7.0 it's missing a line like: + //(NEEDED) Shared library: [libncurses.so.6] + //which exists on Gentoo/NixOS for example. + //So we must link with ncurses lib on try_link() to avoid unresolved symbols + //from 'menu' lib (which we do now as second try if just -lmenu linking fails), + //which also means we must know if we have to use ncurses lib override before even trying + //to link menu,panel,tinfo, and thus need to pass the ncurses lib name to try_link() as third arg. + if let Some(needed_ncurses) = + try_link(sublib_fallback_name, ncurses_lib, ncurses_lib_name_to_use) + { + let extra: String = if needed_ncurses { + format!(", but needed '{}' to link without undefined symbols(known to be true on OpenBSD)", ncurses_lib_name_to_use) + } else { + String::new() + }; + cargo_warn!("Using lib fallback '{}' which links successfully{}. The need for fallback suggests that you might be missing `pkg-config`/`pkgconf` or if they're not missing then you might not have a file named '{}.pc'.", sublib_fallback_name, extra, sublib_fallback_name); + } else { + cargo_warn!("Possibly missing lib for the '{}' feature, and couldn't find its fallback lib name '{}' but we're gonna use it anyway thus compilation is likely to fail below because of this. You might need installed ncurses and pkg-config/pkgconf to fix this.", feature_name, sublib_fallback_name); + } + //We still try linking with it anyway, in case our try_link() code is somehow wrong, + //like it doesn't include some link searchdir paths that are somehow included + //otherwise. Or, as it used to happen before i fixed it: it fails to link because + //libmenu has undefined symbols if not also linked with libncurses which is what + //happens on openbsd; fixed now by always linking with libncurses via try_link() above + println!("cargo:rustc-link-lib={}", sublib_fallback_name); + } // else,if found, it's already emitted the println to tell cargo to link what it found. +} + +//cargo won't run doc tests inside build.rs +/// Creates file with the specified contents. +/// Any existing file with that name is lost. +/// Panics if `file_name` isn't prefixed by the value of OUT_DIR (at runtime) for extra safety. +fn overwrite_file_contents>(file_name: P, contents: &[u8]) { + //Note: asserts in build.rs appear to be enabled even for cargo build --release, and can't be disabled(which is good, we want them on, always) + let file_name = file_name.as_ref(); + assert!( + file_name.starts_with(&get_out_dir()), + "The file name you wanted to create {:?} should be created in OUT_DIR only", + file_name + ); + //PathBuf::display() replaces '\xFF' with the placeholder char which is the replacement character \u{FFFD} + //and also \0 aren't seen at all in the output. Therefore it's best to use {:?} debug to see + //them escaped! Debug already shows double quotes around it. + //On another note: many other programs break at compile time if path contains non-utf8 chars, + //before we even get here in this build.rs! + let mut file = File::create(file_name) + .unwrap_or_else(|err| panic!("Couldn't create file {:?}, reason: '{}'", file_name, err)); + + file.write_all(contents).unwrap_or_else(|err| { + panic!( + "Couldn't write contents to file {:?}, reason: '{}'", + file_name, err + ) + }); + drop(file); //explicit file close, not needed since it's in a function now! +} + +/// returns value of OUT_DIR env. var. as Path +fn get_out_dir() -> impl AsRef { + use std::path::PathBuf; + //OUT_DIR is set by cargo during `cargo build` while build.rs' bin gets executed, but not during + //the compilation(ie. env!("OUT_DIR") isn't set!) + const ENV_NAME_OF_OUT_DIR: &str = "OUT_DIR"; + let env_value:String=env::var(ENV_NAME_OF_OUT_DIR).unwrap_or_else(|err| { + panic!( + "Cannot get env.var. '{}', reason: '{}'. Use `cargo build` instead of running this build script binary directly!", + ENV_NAME_OF_OUT_DIR, err + ) + }); + assert!(!env_value.is_empty(), "OUT_DIR env. var was set to empty."); + PathBuf::from(env_value) + //^ &PathBuf implements AsRef +} + +fn get_linker_searchdirs(from_lib: &Option) -> Vec { + let mut linker_searchdir_args: Vec = Vec::new(); + if let Some(lib) = from_lib { + for link_path in &lib.link_paths { + linker_searchdir_args.push("-L".to_string()); + //Must not use link_path.display() which does lossy conversion to UTF-8, else errors + //might slip through unnoticed and might be harder to track, when paths aren't UTF-8 valid. + //Can however use .display() in other parts of the code that only need to + //show the path in a message. + linker_searchdir_args.push(link_path.to_str().unwrap_or_else(|| { + //XXX: We use debug {:?} to show the path because .display() wouldn't show for example + //\0 in the path, well, it's invisible on the terminal. + panic!("!!! Lib link path {:?} contains invalid UTF-8. This is likely an existing path on you system, so to get rid of this, you'd have to fix/rename the path to be UTF-8. This path was likely found via `pkg-config`.", link_path); + }).to_string()); + } + } + linker_searchdir_args } -fn gen_constants() { - println!("cargo:rerun-if-changed=src/genconstants.c"); - let out_dir = env::var("OUT_DIR").expect("cannot get OUT_DIR"); - let bin = format!("{}", Path::new(&out_dir).join(if cfg!(windows) { "genconstants.exe" } else { "genconstants" }).display()); - let src = format!("{}", Path::new(&out_dir).join("raw_constants.rs").display()); +/// Tries to see if linker can find/link with the named library, to create a binary. +/// Uses ncurses lib searchdirs(if any found by pkg-config) to find that lib. +/// This is mainly used when pkg-config is missing but should still work if pkg-config exists. +/// Returns Some(nn) if linking succeeded, None otherwise, nn is a bool saying if ncurses was needed to can link. +/// Will try to link twice if linking with only that lib fails, the second try adds ncurses lib to +/// linking command, because the lib might depend on it even though it doesn't say(inside it) that +/// it does (this is what happens on OpenBSD). +fn try_link( + lib_name: &str, + ncurses_lib: &Option, + ncurses_lib_name_to_use: &str, +) -> Option { + let out_dir = get_out_dir(); + assert!( + !ncurses_lib_name_to_use.is_empty(), + "You passed empty ncurses lib name string." + ); + + //We won't execute it though, so doesn't matter if it's .exe for Windows + let out_bin_fname = format!("try_link_with_{}", lib_name); + + //we'll generate this .c file with our contents + let out_src_full = Path::new(out_dir.as_ref()).join(format!("{}.c", out_bin_fname)); + + let source_code = b"int main(void) { return 0; }"; + overwrite_file_contents(&out_src_full, source_code); + //TODO: remove commented out code everywhere in build.rs let build = cc::Build::new(); - let compiler = build.try_get_compiler().expect("Failed Build::try_get_compiler"); - let mut command = compiler.to_command(); - if let Ok(x) = std::env::var("NCURSES_RS_CFLAGS") { - command.args(x.split(" ")); - } + let mut command = get_the_compiler_command_from_build(&build); + + let out_bin_full = Path::new(out_dir.as_ref()).join(out_bin_fname); + //Create a bin(not a lib) from a .c file + //though it wouldn't matter here if it's bin or lib, I'm + //not sure how to find its exact output name after, to delete it. + //Adding the relevant args for the libs that we depend upon such as ncurses + // + //First we try to link just the requested lib, eg. 'menu' or 'panel' or 'tinfo' + //if this fails, then we try adding 'ncurses' to libs to link, thus + //on OpenBSD for example, where libmenu doesn't say it depends on the 'ncurses' lib + //it can link successfully because we manually link with it, thus linker won't fail with unresolved symbols. + command + .arg("-o") + .arg_checked(&out_bin_full) + .arg_checked(&out_src_full) + .args_checked(["-l", lib_name]); //this might require the ncurses lib below(on openbsd for sure!) + + //Add linker paths from ncurses lib, if any found! ie. -L + //(this likely will be empty if pkg-config doesn't exist) + //Include paths(for headers) don't matter! ie. -I + //Presumably the other libs(menu,panel,tinfo) are in the same dir(s) as the ncurses lib, + //because they're part of ncurses even though they're split on some distros/OSs. + let linker_searchdir_args: Vec = get_linker_searchdirs(ncurses_lib); + if !linker_searchdir_args.is_empty() { + command.args_checked(linker_searchdir_args); + } + + //this copy will link only the lib, without ncurses + let mut command_copy: Command = command.make_a_partial_copy(); //woulda been too easy if had .clone() - command.arg("-o").arg(&bin).arg("src/genconstants.c").arg("-lcurses"); - assert!(command.status().expect("compilation failed").success()); + //we add ncurses to linked libs, but we only call this if the first try(ie. that copy) fails. + command.args_checked(["-l", ncurses_lib_name_to_use]); + let exit_status = command_copy.status_or_panic("compilation(without ncurses lib)"); //runs compiler + let mut ret: bool = exit_status.success(); + let mut requires_ncurses_lib: bool = false; + if !ret { + //first try failed, try second with -lncurses(w) added. + let exit_status = command.status_or_panic("compilation(with ncurses lib)"); //runs compiler + ret = exit_status.success(); + if ret { + //cargo_warn!(""); + requires_ncurses_lib = true; + } + } - let consts = Command::new(&bin).output() - .expect(&format!("{} failed", bin)); + if DELETE_GENERATEDS { + if ret { + //delete temporary bin that we successfully generated + std::fs::remove_file(&out_bin_full).unwrap_or_else(|err| { + panic!( + "Cannot delete generated bin file {:?}, reason: '{}'", + out_bin_full, err + ) + }); + } + //delete the .c that we generated + std::fs::remove_file(&out_src_full).unwrap_or_else(|err| { + panic!( + "Cannot delete generated C file {:?}, reason: '{}'", + out_src_full, err + ) + }); + } - let mut file = File::create(&src).unwrap(); - - file.write_all(&consts.stdout).unwrap(); + if ret { + Some(requires_ncurses_lib) + } else { + None + } } -fn gen_menu_constants() { - println!("cargo:rerun-if-changed=src/menu/genconstants.c"); - let out_dir = env::var("OUT_DIR").expect("cannot get OUT_DIR"); - let bin = format!("{}", Path::new(&out_dir).join(if cfg!(windows) { "genmenuconstants.exe" } else { "genmenuconstants" }).display()); - let src = format!("{}", Path::new(&out_dir).join("menu_constants.rs").display()); +/// Emits "cargo:rerun-if-env-changed=ENV_VAR" on stdout on every call, cargo doesn't mind. +// cargo doesn't mind repetitions, see this: cargo clean;cargo build -vv |& grep rerun-if-env-changed +fn watch_env_var(env_var: &'static str) { + assert!(!env_var.is_empty(), "Passed empty env.var. to watch for."); + println!("cargo:rerun-if-env-changed={}", env_var); +} - let build = cc::Build::new(); - let compiler = build.try_get_compiler().expect("Failed Build::try_get_compiler"); - let mut command = compiler.to_command(); +/// set some sensible defaults +fn new_build(lib: &Option) -> cc::Build { + //XXX: Note: env.var. "CC" can override the compiler used and will cause rebuild if changed. + let mut build = cc::Build::new(); + if let Some(lib) = lib { + //header file paths eg. for ncurses.h + build.includes(&lib.include_paths); + //for path in lib.include_paths.iter() { + // build.include(path); + //} + } + build.opt_level(1); //else is 0, causes warning on NixOS: _FORTIFY_SOURCE requires compiling with optimization (-O) + + build.define("DEBUG", None); //-DDEBUG so that asserts are enabled for sure! + + //XXX:Don't have to emit cargo:rerun-if-env-changed= here because try_flags_from_environment() + //below does it for us, however it does it on every call! (unless Build::emit_rerun_if_env_changed(false)) + //but if an overriding variant of it is defined like NCURSES_RS_CFLAGS_x86_64_unknown_linux_gnu + //then this weaker one won't be emitted because the override will be the only one in effect. + //We could forcefully emit anyway, but no point, it will be ignored and just rebuild for no reason. + //watch_env_var(ENV_VAR_NAME_FOR_NCURSES_RS_CFLAGS); + + //See comment above the const var def. to understand which env.vars are tried here: + let _ = build.try_flags_from_environment(ENV_VAR_NAME_FOR_NCURSES_RS_CFLAGS); + + //these two are already in from the default Build + //build.flag_if_supported("-Wall"); + //build.flag_if_supported("-Wextra"); + build.flag_if_supported("-Wpedantic"); + //build.flag_if_supported("-Wstrict-prototypes");//maybe fix me: triggers warnings in wrap.c + build.flag_if_supported("-Weverything"); //only clang + + build +} + +fn build_wrap(ncurses_lib: &Option) { + // build.file(source_file), below, doesn't emit this: + println!("cargo:rerun-if-changed=src/wrap.c"); + let mut build = new_build(ncurses_lib); + + // The following creates `libwrap.a` on linux, a static lib + build.file("src/wrap.c").compile("wrap"); + //the resulting lib will be kept until deleted by 'cargo clean' +} + +fn get_the_compiler_command_from_build(build: &cc::Build) -> std::process::Command { + //'cc::Build' can do only lib outputs but we want a binary + //so we get the command (and args) thus far set and add our own args. + //Presumably all args will be kept, as per: https://docs.rs/cc/1.0.92/cc/struct.Build.html#method.get_compiler + //(though at least the setting for build.file(source_c_file) won't be, + // but we don't use that way and instead set it later as an arg to compiler) + let compiler = build + .try_get_compiler() + .expect("Failed Build::try_get_compiler"); + compiler.to_command() +} + +/// Compiles an existing .c file, runs its bin to generate a .rs file from its output. +/// Uses ncurses include paths and links with ncurses lib(s) +// Note: won't link with tinfo unless pkg-config returned it. +// ie. if `pkg-config ncurses --libs` shows: -lncurses -ltinfo +// So even though we used a fallback tinfo in main, for cargo, it won't be used here. FIXME: if tinfo is needed here ever! (it's currently not, btw) +fn gen_rs( + source_c_file: &str, + out_bin_fname: &str, + gen_rust_file: &str, + ncurses_lib: &Option, + lib_name: &str, + additional_msg_when_non_zero_exit_code: &str, +) { + //Build::file() doesn't already emit this: + println!("cargo:rerun-if-changed={}", source_c_file); + let out_dir = get_out_dir(); + #[cfg(windows)] + let out_bin_fname = format!("{}.exe", out_bin_fname); //shadowed + let bin_full = Path::new(out_dir.as_ref()).join(out_bin_fname); + + let build = new_build(ncurses_lib); - if let Ok(x) = std::env::var("NCURSES_RS_CFLAGS") { - command.args(x.split(" ")); - } + let mut command = get_the_compiler_command_from_build(&build); - command.arg("-o").arg(&bin).arg("src/menu/genconstants.c").arg("-lcurses"); - assert!(command.status().expect("compilation failed").success()); + //create a bin(not a lib) from a .c file + //adding the relevant args for the libs that we depend upon such as ncurses + command + .arg("-o") + .arg_checked(&bin_full) + .arg_checked(source_c_file) + .args_checked(["-l", lib_name]); + let linker_searchdir_args: Vec = get_linker_searchdirs(ncurses_lib); + if !linker_searchdir_args.is_empty() { + command.args_checked(linker_searchdir_args); + } + command.success_or_panic("compilation"); //runs compiler - let consts = Command::new(&bin).output() - .expect(&format!("{} failed", bin)); + //Execute the compiled binary, panicking if non-zero exit code, else compilation will fail + //later with things like: "error[E0432]: unresolved import `constants::TRUE`" in the case of + //generating raw_constants.rs which would be empty due to 'genconstants' having failed with exit + //code 1 because env.var. TERM=a_terminal_not_in_term_database + let output: std::process::Output = Command::new(&bin_full).output_success_or_panic( + &format!( + "the binary(compiled from the existing C file '{}')", + source_c_file + ), + additional_msg_when_non_zero_exit_code, + ); - let mut file = File::create(&src).unwrap(); - - file.write_all(&consts.stdout).unwrap(); + //Write the output from executing the binary into a new rust source file .rs + //That .rs file is later used outside of this build.rs, in the normal build + let gen_rust_file_full_path = Path::new(out_dir.as_ref()).join(gen_rust_file); + overwrite_file_contents(gen_rust_file_full_path, &output.stdout); + //we ignore stderr. + //we don't delete this file because it's used to compile the rest of the crate. } fn check_chtype_size(ncurses_lib: &Option) { - let out_dir = env::var("OUT_DIR").expect("cannot get OUT_DIR"); - let src = format!("{}", Path::new(&out_dir).join("chtype_size.c").display()); - let bin = format!("{}", Path::new(&out_dir).join(if cfg!(windows) { "chtype_size.exe" } else { "chtype_size" }).display()); + let out_dir = get_out_dir(); + let basename = "chtype_size"; + let src_file_name = format!("{}.c", basename); + let src_full = Path::new(out_dir.as_ref()).join(&src_file_name); + let bin_name = if cfg!(windows) { + format!("{}.exe", basename) + } else { + basename.to_string() + }; + let bin_full = Path::new(out_dir.as_ref()).join(bin_name); - let mut fp = File::create(&src).expect(&format!("cannot create {}", src)); - fp.write_all(b" + let contents = br#"// autogenerated by build.rs #include #include #include #include +//#include //for exit() below, else shows warnings. + int main(void) { + //foofoo //uncomment to see what happens if compiler fails to compile + //assert(false);//to test what happens if execution fails when killed by signal 6 + //exit(23);//to test if bin execution fails with exit code != 0 + if (sizeof(chtype)*CHAR_BIT == 64) { - puts(\"cargo:rustc-cfg=feature=\\\"wide_chtype\\\"\"); + puts("cargo:rustc-cfg=feature=\"wide_chtype\""); } else { /* We only support 32-bit and 64-bit chtype. */ - assert(sizeof(chtype)*CHAR_BIT == 32 && \"unsupported size for chtype\"); + assert(sizeof(chtype)*CHAR_BIT == 32 && "unsupported size for chtype"); } #if defined(NCURSES_MOUSE_VERSION) && NCURSES_MOUSE_VERSION == 1 - puts(\"cargo:rustc-cfg=feature=\\\"mouse_v1\\\"\"); + puts("cargo:rustc-cfg=feature=\"mouse_v1\""); #endif return 0; } - ").expect(&format!("cannot write into {}", src)); +"#; + overwrite_file_contents(&src_full, contents); - let mut build = cc::Build::new(); - if let Some(lib) = ncurses_lib { - for path in lib.include_paths.iter() { - build.include(path); + let build = new_build(ncurses_lib); + + let mut command = get_the_compiler_command_from_build(&build); + + command + .arg("-o") + .arg_checked(&bin_full) + .arg_checked(&src_full); + command.success_or_panic("compilation"); //runs compiler + + let features = Command::new(&bin_full).output_success_or_panic( + &format!( + "the binary(compiled from the generated C file '{}' in the same dir)", + &src_file_name + ), + "", + ); + + //for cargo to consume + print!("{}", String::from_utf8_lossy(&features.stdout)); + + if DELETE_GENERATEDS { + std::fs::remove_file(&src_full).unwrap_or_else(|err| { + panic!( + "Cannot delete generated C file {:?}, reason: '{}'", + src_full, err + ) + }); + std::fs::remove_file(&bin_full).unwrap_or_else(|err| { + panic!( + "cannot delete compiled bin file {:?}, reason: '{}'", + bin_full, err + ) + }); + } +} + +// you must call this only once, to avoid re-printing "cargo:rustc-link-lib=" // FIXME +fn get_ncurses_lib_name(ncurses_lib: &Option) -> String { + //Was it found(and thus printed) by pkg_config::probe_library() ? + let mut already_printed: bool = false; + let lib_name: String; + watch_env_var(ENV_VAR_NAME_FOR_LIB); + match std::env::var(ENV_VAR_NAME_FOR_LIB) { + Ok(value) => lib_name = value, + Err(_) => { + if let Some(ref lib) = ncurses_lib { + // if here, `pkg-config`(shell command) via pkg_config crate, + // has found the ncurses lib (eg. via the `ncurses.pc` file) + // You can get something like this ["ncurses", "tinfo"] as the lib.libs vector + // but we shouldn't assume "ncurses" is the first ie. lib.libs[0] + // and the exact name of it can be ncurses,ncursesw,ncurses5,ncursesw5 ... + // so find whichever it is and return that: + let substring_to_find = "curses"; + if let Some(found) = lib.libs.iter().find(|&s| s.contains(substring_to_find)) { + //If we're here, the function calls to pkg_config::probe_library() + //from above ie. through find_library(), have already printed these: + // cargo:rustc-link-lib=ncurses + // cargo:rustc-link-lib=tinfo + //so there's no need to re-print the ncurses line as it would be the same. + already_printed = true; + lib_name = found.clone(); + } else { + // Construct the repeated pkg-config command string + let repeated_pkg_config_command: String = NCURSES_LIB_NAMES + .iter() + .map(|ncurses_lib_name| format!("pkg-config --libs {}", ncurses_lib_name)) + .collect::>() + .join("` or `"); + + panic!( + "pkg_config(crate) reported that it found the ncurses lib(s) but the substring '{}' was not among them, ie. in the output of the shell command(s) eg. `{}`\n + Try setting NCURSES_NO_PKG_CONFIG=1 and/or NCURSESW_NO_PKG_CONFIG=1 to disable pkg-config and thus allow for the fallback to lib name 'ncurses' respectively 'ncursesw' to be tried. Or fix ncurses.pc or ncursesw.pc file.", + substring_to_find, + repeated_pkg_config_command + ); + } + } else { + //pkg-config didn't find the lib, fallback to 'ncurses' or 'ncursesw' + let what_lib = NCURSES_LIB_NAME_FALLBACK.to_string(); + // On FreeBSD it works without pkgconf and ncurses(6.4) installed but it will fail + // to link ex_5 with 'menu' lib, unless `NCURSES_RS_RUSTC_FLAGS="-lmenu" is set. + // this is why we now use fallbacks for 'menu' and 'panel` above too(not just for 'ncurses' lib) + // that is, when pkgconf or pkg-config are missing, yet the libs are there. + // Print the warning message, but use old style warning with one ":" not two "::", + // because old cargos(pre 23 Dec 2023) will simply ignore it and show no warning if it's "::" + cargo_warn!("Using (untested)fallback lib name '{}' but if compilation fails below(like when linking ex_5 with 'menu' feature), that is why. It's likely you have not installed one of ['pkg-config' or 'pkgconf'], and/or 'ncurses' (it's package 'ncurses-devel' on Fedora). This seems to work fine on FreeBSD 14 regardless, however to not see this warning and to ensure 100% compatibility(on any OS) be sure to install, on FreeBSD, at least `pkgconf` if not both ie. `# pkg install ncurses pkgconf`.", what_lib); + //fallback lib name: 'ncurses' or 'ncursesw' + //if this fails later, there's the warning above to get an idea as to why. + lib_name = what_lib; + } } + }; + if !already_printed { + //TODO: try_link() ? then refactor the warning messages. Well, now that try_link already links ncurses lib, this isn't needed? or if it is, don't specify -l twice for it inside try_link + println!("cargo:rustc-link-lib={}", lib_name); } - let compiler = build.try_get_compiler().expect("Failed Build::try_get_compiler"); - let mut command = compiler.to_command(); + lib_name +} + +//trait MyOutput {} +// +//impl MyOutput for std::process::Output {} + +trait MyExitStatus { + fn success_or_panic(self) -> ExitStatus; +} - if let Ok(x) = std::env::var("NCURSES_RS_CFLAGS") { - command.args(x.split(" ")); +impl MyExitStatus for std::process::ExitStatus { + fn success_or_panic(self) -> ExitStatus { + if self.success() { + self + } else { + let how: String; + if let Some(code) = self.code() { + how = format!(" with exit code {}.", code); + } else { + how = ", was it terminated by a signal?!".to_string(); + } + panic!( + "!!! Compiler failed{} Is ncurses installed? \ + pkg-config or pkgconf too? \ + it's 'ncurses-devel' on Fedora; \ + and 'libncurses-dev' on Ubuntu; \ + run `nix-shell` first, on NixOS. \ + Or maybe it failed for different reasons which are seen in the errored output above.", + how + ) + } } +} - command.arg("-o").arg(&bin).arg(&src); - assert!(command.status().expect("compilation failed").success()); - let features = Command::new(&bin).output() - .expect(&format!("{} failed", bin)); - print!("{}", String::from_utf8_lossy(&features.stdout)); +// Define an extension trait for Command +trait MyCompilerCommand { + fn output_or_panic(&mut self, what_kind_of_process_is_it: &str) -> std::process::Output; + fn output_success_or_panic( + &mut self, + what_kind_of_process_is_it: &str, + additional_msg_when_non_zero: &str, + ) -> std::process::Output; + fn success_or_panic(&mut self, what_kind_of_command_is_it: &str) -> ExitStatus; + //fn success_or_else ExitStatus>(&mut self, op: F) -> ExitStatus; + fn just_status_or_panic(&mut self, what_kind_of_command_is_it: &str) -> ExitStatus; + fn status_or_panic(&mut self, what_kind_of_command_is_it: &str) -> ExitStatus; + fn show_what_will_run(&mut self) -> &mut Self; + fn get_program_or_panic(&self) -> &str; + fn get_what_will_run(&self) -> (String, MyArgs, Option<&Path>, MyEnvVars); + fn assert_no_nul_in_args(&mut self) -> &mut Self; + /// Panics if arg has \0 in it. + fn args_checked(&mut self, args: I) -> &mut Command + where + I: IntoIterator, + S: AsRef; + /// Panics if arg has \0 aka NUL in it, + /// otherwise the original `Command::arg()` would've set it to "" + /// Doesn't do any other checks, passes it to `Command::arg()` + fn arg_checked>(&mut self, arg: S) -> &mut Command; + fn panic(&mut self, err: T, what_type_of_command: &str) -> !; + fn make_a_partial_copy(&self) -> Self; +} + +fn has_null_byte>(arg: S) -> bool { + let os_str = arg.as_ref(); + #[cfg(not(target_os = "windows"))] + for &byte in os_str.as_bytes() { + if byte == 0 { + return true; + } + } + false +} + +/// Args with \0 in them, passed to `std::process::Command::arg()` or `std::process::Command::args()` +/// get replaced(by those calls)entirely with this: "" +const REPLACEMENT_FOR_ARG_THAT_HAS_NUL: &str = ""; +// Implement the extension trait for Command, so you can use methods on a Command instance even +// though it's a type that's not defined here but in std::process +impl MyCompilerCommand for std::process::Command { + /// Executes `Command::output()` and gives you Output struct or panics + /// but the exit code may not have been 0 + fn output_or_panic(&mut self, what_kind_of_process_is_it: &str) -> std::process::Output { + self.output().unwrap_or_else(|err| { + self.panic(err, what_kind_of_process_is_it); + }) + } + + /// Executes `Command::output()` and gives you Output struct or panics + /// also panics if exit code was not 0 and shows you stdout/stderr if so. + /// The extra args are to be displayed in cases of errors. + fn output_success_or_panic( + &mut self, + what_kind_of_process_is_it: &str, + additional_msg_when_non_zero_exit_code: &str, + ) -> std::process::Output { + let output = self.output_or_panic(what_kind_of_process_is_it); + // test this with: `$ TERM=foo cargo build` + let show_stdout_stderr = || { + //XXX: presumably eprintln! and std::io::stderr().write_all().unwrap() write to same stderr + //stream and both would panic if some error would happen when writing to it! + eprintln!("But here's its stdout&stderr:"); + eprintln!("|||stdout start|||"); + //Preserve stdout/stderr bytes, instead of lossily convert them to utf-8 before showing them. + //show stdout of executed binary, on stderr + std::io::stderr().write_all(&output.stdout).unwrap(); + eprintln!("\n|||stdout end||| |||stderr start|||"); + //show stderr of executed binary, on stderr + std::io::stderr().write_all(&output.stderr).unwrap(); + eprintln!("\n|||stderr end|||"); + }; + let prog = self.get_program_or_panic(); + let and_panic = || -> ! { + panic!( + "due to the above-reported error while executing {} '{}'.", + what_kind_of_process_is_it, prog + ); + }; + + let exit_code = output.status.code().unwrap_or_else(|| { + //we get here if it segfaults(signal 11), so if exited due to signal + //but unsure if we get here for any other reasons! + //To test this branch uncomment a segfault line early in src/genconstants.c then `cargo build` + + let basename=Path::new(prog).file_name().unwrap_or_else(|| { + eprintln!("Couldn't get basename for {} '{}'", what_kind_of_process_is_it,prog); + OsStr::new("") //refusing to panic over this + }); + let basename=basename.to_str().unwrap_or_else(|| { + eprintln!("Couldn't convert OsStr '{:?}' to &str, while processing the file name of {} '{}'", basename, what_kind_of_process_is_it,prog); + "" //refusing to panic over this + }); + eprintln!( + "!!! Execution of {} '{}' failed, likely killed by signal! Maybe check 'dmesg' for the word \"segfault\" or \"{}\". We can't know here, which signal happened.", + what_kind_of_process_is_it, + prog, basename + ); + show_stdout_stderr(); + and_panic(); + }); + if 0 == exit_code { + #[allow(clippy::needless_return)] // it's more readable + return output; + } // else { + eprintln!( + "!!! Execution of {} '{}' failed with exit code '{}'", + what_kind_of_process_is_it, prog, exit_code + ); + show_stdout_stderr(); + eprintln!("{}", additional_msg_when_non_zero_exit_code); + and_panic(); + } + + /// Executes `Command::status().success()` and panics if it any fail + /// This means exit code 0 is ensured. + /// Note: You can't use an arg value "", or this will panic. + fn success_or_panic(&mut self, what_kind_of_command_is_it: &str) -> ExitStatus { + let exit_status: ExitStatus = self + .status_or_panic(what_kind_of_command_is_it) + .success_or_panic(); + exit_status + } + + //XXX: can't override arg/args because they're not part of a Trait in Command + //so would've to wrap Command in my own struct for that. This would've ensured + //that any added args were auto-checked. + /// panics if any args have \0 aka nul in it, else Command will panic later, on execution. + fn args_checked(&mut self, args: I) -> &mut Command + where + I: IntoIterator, + S: AsRef, + { + for arg in args { + self.arg_checked(arg.as_ref()); + } + self + } + + /// panics if arg has \0 aka nul in it, else Command will panic later, on execution. + fn arg_checked>(&mut self, arg: S) -> &mut Command { + #[allow(clippy::manual_assert)] + if has_null_byte(&arg) { + //If the arg has NUL ie. \0 in it then arg got replaced already + //with "", internally, by std::process::Command::arg() . + //The found arg here will be shown with \0 in this Debug way. + panic!( + "Found arg '{:?}' that has at least one \\0 aka nul in it! \ + This would've been silently replaced with '{}' and error later if at all.", + arg.as_ref(), + REPLACEMENT_FOR_ARG_THAT_HAS_NUL + ); + } + self.arg(arg) + } + + /// Beware if user set the arg on purpose to the value of `REPLACEMENT_FOR_ARG_THAT_HAS_NUL` + /// which is "" then this will panic, it's a false positive. + fn assert_no_nul_in_args(&mut self) -> &mut Self { + let args = self.get_args(); + for (count, arg) in args.enumerate() { + if let Some(fully_utf8_arg) = arg.to_str() { + //If the arg had NUL ie. \0 in it then arg got replaced already + //with "", internally, by std::process::Command::arg() . + #[allow(clippy::manual_assert)] + if fully_utf8_arg == REPLACEMENT_FOR_ARG_THAT_HAS_NUL { + panic!( + "Found arg number '{}' that has \\0 aka NUL in it! \ + It got replaced with '{}'.", + count + 1, + REPLACEMENT_FOR_ARG_THAT_HAS_NUL + ); + } + } + } + self + } + + fn get_program_or_panic(&self) -> &str { + let program = self.get_program(); + let p_prog = program + .to_str() + .unwrap_or_else(|| panic!("Executable {:?} isn't valid rust string", program)); + p_prog + } + + fn get_what_will_run(&self) -> (String, MyArgs, Option<&Path>, MyEnvVars) { + let prog = self.get_program_or_panic().to_string(); + + //If an arg had NUL ie. \0 in it then arg got replaced already + //with "", internally, by std::process::Command::arg() + //if it was added via Command::arg() or Command::args(). + //To prevent that use Command::arg_checked() and ::args_checked() + let args: MyArgs = self.get_args().collect(); + //let args: MyArgs = self.get_args().collect::>().to_my_args(); + + let cur_dir = self.get_current_dir(); + let env_vars: MyEnvVars = self.get_envs().collect(); + + //FIXME: make this a struct so the order doesn't get confused in callers. + + //return this tuple + (prog, args, cur_dir, env_vars) + } + + /// just like `Command::status()` but panics if it can't execute it, + /// ie. if `status()` would've returned an Err + /// returns `ExitStatus` whether it be 0 or !=0 + /// Doesn't show you what will be executed and doesn't check args. + /// (not meant to be used outside) + fn just_status_or_panic(&mut self, what_kind_of_command_is_it: &str) -> ExitStatus { + // Call the original status() method and handle the potential error + self.status().unwrap_or_else(|err| { + self.panic(err, what_kind_of_command_is_it); + }) + } + + /// Shows command that will execute and checks args, only after this + /// it's gonna be trying to do `.status()` + /// Panics if status would've returned an Err + fn status_or_panic(&mut self, what_kind_of_command_is_it: &str) -> ExitStatus { + self.show_what_will_run() + .assert_no_nul_in_args() + .just_status_or_panic(what_kind_of_command_is_it) + } + + /// (not meant to be used outside) + //panic( <- for search + fn panic(&mut self, err: T, what_type_of_command: &str) -> ! { + let (p_prog, args, cur_dir, env_vars) = self.get_what_will_run(); + let how_many_args = args.len(); + let extra_space = if what_type_of_command.is_empty() { + "" + } else { + " " + }; + let (cur_dir_for_print, env_vars_for_print) = get_cd_and_env_for_print(cur_dir, &env_vars); + panic!( + "Failed to run {}{}command '{}' with '{}' args: '{}'{}{}, reason: '{}'", + what_type_of_command, + extra_space, + p_prog, + how_many_args, + args, + cur_dir_for_print, + env_vars_for_print, + err + ) + } + + /// shows on stderr, which command will be executed. + fn show_what_will_run(&mut self) -> &mut Self { + let (exe_name, args, cur_dir, env_vars) = self.get_what_will_run(); + let how_many_args = args.len(); + let (cur_dir_for_print, env_vars_for_print) = get_cd_and_env_for_print(cur_dir, &env_vars); + eprintln!( + "!! Next, attempting to run command '{}' with '{}' args: '{}'{}{}.", + exe_name, how_many_args, args, cur_dir_for_print, env_vars_for_print + ); + self + } + + /// copies exe,args,cwd and env.vars + fn make_a_partial_copy(&self) -> Self { + let prog = self.get_program(); + let mut r#new = Command::new(prog); + r#new.args_checked(self.get_args()); + //r#new.envs(self.get_envs().collect::>()); + for (k, v) in self.get_envs() { + if let Some(v) = v { + r#new.env(k, v); + } else { + r#new.env_remove(k); + } + } + if let Some(cd) = self.get_current_dir() { + r#new.current_dir(cd); + } + + r#new + } +} + +//"We can't directly implement Display for Vec<&OsStr> because Vec and OsStr are not types defined in our crate." +// Define a custom type to represent a vector of OsStr references +struct MyArgs<'a>(Vec<&'a OsStr>); + +fn humanly_visible_os_chars(f: &mut fmt::Formatter<'_>, arg: &OsStr) -> fmt::Result { + if let Some(arg_str) = arg.to_str() { + // If the argument is valid UTF-8, + if arg_str.contains('\0') { + write!(f, "\"")?; + // has \0 in it, show them as \x00 but keep the rest as they are such as ♥ + for c in arg_str.chars() { + if c == '\0' { + write!(f,"\\x00")? + } else { + write!(f,"{}",c)? + } + } + write!(f, "\"")?; + } else { + // has no \0 in it + write!(f, "\"{}\"", arg_str)?; + } + } else { + //None aka not fully utf8 arg + //then we show it as ascii + hex + write!(f, "\"")?; + #[cfg(not(target_os = "windows"))] + for byte in arg.as_bytes() { + match std::char::from_u32(u32::from(*byte)) { + //chars in range 0x20..0x7E (presumably printable) are shown as they are + Some(c) if (*byte >= 0x20) && (*byte <= 0x7E) => write!(f, "{}", c)?, + //anything else including \0 and ♥ become hex, eg. ♥ is \xE2\x99\xA5 + _ => { + write!(f, "\\x{:02X}", byte)?; + } + } + } + write!(f, "\"")?; + } + Ok(()) +} + +// Implement the Display trait for MyArgs +impl<'a> fmt::Display for MyArgs<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let cached_last_index = self.len() - 1; + for (index, arg) in self.iter().enumerate() { + humanly_visible_os_chars(f, arg)?; + // Add a space between arguments, except for the last one + if index < cached_last_index { + write!(f, " ")?; + } + } + Ok(()) + } +} + +/// but this is better: +impl<'a> FromIterator<&'a OsStr> for MyArgs<'a> { + fn from_iter>(iter: I) -> Self { + MyArgs(iter.into_iter().collect()) + } +} + +//so we don't have to use self.0 +impl<'a> std::ops::Deref for MyArgs<'a> { + type Target = Vec<&'a OsStr>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} - std::fs::remove_file(&src).expect(&format!("cannot delete {}", src)); - std::fs::remove_file(&bin).expect(&format!("cannot delete {}", bin)); +struct MyEnvVars<'a>(Vec<(&'a OsStr, Option<&'a OsStr>)>); + +impl<'a> std::ops::Deref for MyEnvVars<'a> { + type Target = Vec<(&'a OsStr, Option<&'a OsStr>)>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'a> FromIterator<(&'a OsStr, Option<&'a OsStr>)> for MyEnvVars<'a> { + fn from_iter)>>(iter: I) -> Self { + MyEnvVars(iter.into_iter().collect()) + } } + +impl<'a> fmt::Display for MyEnvVars<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (key, value) in self.iter() { + match value { + Some(value) => { + write!(f, "(set) ")?; + humanly_visible_os_chars(f, key)?; + write!(f, "=")?; + humanly_visible_os_chars(f, value)?; + writeln!(f)?; + } + None => { + // This is how an env. var. is said to be removed, apparently. + // ie. if parent had it, it's not inherited? somehow. (untested) + write!(f, "(del) ")?; + humanly_visible_os_chars(f, key)?; + writeln!(f)?; + } + } + } + Ok(()) + } +} + +/// +fn get_cd_and_env_for_print(cur_dir: Option<&Path>, env_vars: &MyEnvVars) -> (String, String) { + let cur_dir_for_print: String = if let Some(dir) = cur_dir { + format!(", in current dir: {:?}", dir) + } else { + format!( + ", in unspecified current dir(but the actual cwd is currently {:?})", + env::current_dir() + ) + }; + let formatted_env_vars_for_print: String = if env_vars.is_empty() { + ", with no extra env.vars added or deleted(so all are inherited from parent process)" + .to_string() + } else { + format!(", with env.vars: '{}'", env_vars) + }; + (cur_dir_for_print, formatted_env_vars_for_print) +} + diff --git a/examples/ex_5.rs b/examples/ex_5.rs index 16658af4..293245a0 100644 --- a/examples/ex_5.rs +++ b/examples/ex_5.rs @@ -1,6 +1,7 @@ #[allow(unused_imports)] extern crate ncurses; +#[cfg(feature="menu")] use ncurses::*; #[cfg(feature="menu")] @@ -38,8 +39,8 @@ fn main() { /* Print a border around the main window */ box_(my_menu_win, 0, 0); - mvprintw(LINES() - 3, 0, "Press to see the option selected"); - mvprintw(LINES() - 2, 0, "F1 to exit"); + mvprintw(LINES() - 3, 0, "Press to see the option selected").unwrap();//safe + mvprintw(LINES() - 2, 0, "F1 to exit").unwrap(); //safe refresh(); /* Post the menu */ @@ -60,7 +61,8 @@ fn main() { 10 => {/* Enter */ mv(20, 0); clrtoeol(); - mvprintw(20, 0, &format!("Item selected is : {}", item_name(current_item(my_menu)))[..]); + mvprintw(20, 0, &format!("Item selected is : {}", item_name(current_item(my_menu)))[..]).unwrap(); + //unwrap() here is safe unless the items have any \0 (nul) char in them, then it will panic! pos_menu_cursor(my_menu); }, _ => {} diff --git a/examples/ex_7.rs b/examples/ex_7.rs index f1b802a2..ec885f85 100644 --- a/examples/ex_7.rs +++ b/examples/ex_7.rs @@ -10,6 +10,7 @@ extern crate ncurses; +#[cfg(feature = "wide")] use std::char; use ncurses::*; @@ -17,7 +18,7 @@ use ncurses::*; fn main() { let locale_conf = LcCategory::all; - setlocale(locale_conf, "en_US.UTF-8"); + setlocale(locale_conf, "en_US.UTF-8").unwrap();//safe due to this &str having no \0 in it /* Setup ncurses. */ initscr(); @@ -56,7 +57,7 @@ fn main() Some(WchResult::Char(c)) => { /* Enable attributes and output message. */ - addstr("\nKey pressed: "); + addstr("\nKey pressed: ").unwrap();//safe, no \0 in this &str attron(A_BOLD | A_BLINK); addstr(format!("{}\n", char::from_u32(c as u32).expect("Invalid char")).as_ref()).unwrap(); attroff(A_BOLD | A_BLINK); diff --git a/shell.nix b/shell.nix index 0442e4b3..d47cc2f9 100644 --- a/shell.nix +++ b/shell.nix @@ -5,9 +5,10 @@ pkgs.stdenv.mkDerivation name = "ncurses-rs"; buildInputs = with pkgs; [ + pkgs.pkg-config pkgs.cargo pkgs.rustup pkgs.rustfmt - pkgs.ncurses5 + pkgs.ncurses ]; } diff --git a/src/constants.rs b/src/constants.rs index 9d6487b5..7d37b234 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -17,8 +17,8 @@ use super::ll::*; mod wrapped { use libc::{ c_char, c_int }; - use ll::chtype; - use ll::WINDOW; + use crate::ll::chtype; + use crate::ll::WINDOW; extern "C" { @@ -58,7 +58,9 @@ wrap_extern!(LINES: c_int); wrap_extern!(TABSIZE: c_int); pub fn acs_map() -> *const chtype { unsafe { - &wrapped::acs_map as *const chtype + std::ptr::addr_of!(wrapped::acs_map) as *const chtype + // addr_of! needs minimum rust 1.51.0, if want lower, try this instead: + // &wrapped::acs_map as *const chtype } } diff --git a/src/genconstants.c b/src/genconstants.c index f79e2d16..a86ed3b9 100644 --- a/src/genconstants.c +++ b/src/genconstants.c @@ -1,14 +1,67 @@ +#include // for printf/fprintf but curses.h already includes this(everywhere?) #include #define PCONST(ty, NAME) printf("pub const " #NAME ": " #ty " = %lld;\n", (long long) (NAME)) #define PCONSTU(ty, NAME) printf("pub const " #NAME ": " #ty " = %llu;\n", (unsigned long long) (NAME)) -int main() { - /* some values aren't set until after this is run */ - printf("//"); +//XXX: If NAME isn't defined via #define or anything in the ncurses.h file, instead of not defining it at all in our ncurses crate, we define it as being of a specific type(seen below as long snakecase text) which when used by any other crates(like the 'cursive' crate) will compile error in a helpful manner(the type itself is the error message and gives an idea on how to fix it) + +// This long snakecase text below, is a type in rust which will be shown when a crate(like cursive) that uses our crate compiles on a system with old ncurses(like macos Mojave 10.14.6 with its native ncurses version instead of the homebrew ncurses version installed), whereby one or more macro definitions like A_ITALIC aren't defined in ncurses.h +#define ERROR_MSG_TYPE "Your_ncurses_installation_is_too_old_and_it_does_not_have_this_underlined_identifier_defined_in_its_header_file_therefore_the_ncurses_crate_did_not_include_it_because_it_tries_to_be_compatible_however_this_crate_in_which_you_see_the_error_it_needs_the_identifier__If_you_are_on_MacOS_then_use_the_brew_version_of_ncurses_and_set_PKG_CONFIG_PATH_to_the_pkgconfig_dir_from_within_it" +//using these two(alias,constructor) in .rs avoids duplication of the long-named type for every .c defined that we wanna handle +#define ERROR_MSG_TYPE_ALIAS "TypeAliasForErrorMsgType" +#define ERROR_MSG_TYPE_CONSTRUCTOR "ERROR_MSG_TYPE_CONSTRUCTOR" + +//Same deal as before except this(error) is for defined in ncurses.h file that are expected to be missing in latest ncurses version installed in the system... +#define ERROR_MSG_TYPE_EXPECTED_MISS "The_ncurses_crate_did_not_define_this_underlined_identifier_because_your_ncurses_installation_is_up_to_date_and_expectedly_did_not_have_it__however_the_crate_where_you_see_this_error_requires_this_identifier__so_either_upgrade_or_fix_this_crate__or_downgrade_your_ncurses_installation_and_cargo_clean_before_retrying__or_alternatively_try_using_an_older_ncurses_crate_if_you_know_that_worked_before" +#define ERROR_MSG_EXPECTED_MISS_TYPE_ALIAS "TypeAliasForErrorMsgWhenExpectedMissType" +#define ERROR_MSG_EXPECTED_MISS_TYPE_CONSTRUCTOR "ERROR_MSG_EXPECTED_MISS_TYPE_CONSTRUCTOR" + +//Don't forget to call this in main(), one time. +#define DEFINE_RS_TYPES \ + do {\ + printf("/// For MacOS: `brew install ncurses` then `export PKG_CONFIG_PATH=\"/usr/local/opt/ncurses/lib/pkgconfig\"`\n/// that will give you a compatible/newer ncurses installation.\n");\ + printf("#[derive(Debug)] // derive to avoid a warning\n\ +#[allow(non_camel_case_types)] \ +pub struct "ERROR_MSG_TYPE";\n\n\ +type "ERROR_MSG_TYPE_ALIAS" = "ERROR_MSG_TYPE";\n\n\ +#[allow(dead_code)]\n\ +const "ERROR_MSG_TYPE_CONSTRUCTOR": "ERROR_MSG_TYPE_ALIAS" = "ERROR_MSG_TYPE";\n\n\n");\ +printf("///Typically, the crate that errors with the following, should be patched to not use the identifier that's tagged with this type, because the latest ncurses installation doesn't have that identifier definition(anymore).\n");\ +printf("#[derive(Debug)] // derive to avoid a warning\n\ +#[allow(non_camel_case_types)] \ +pub struct "ERROR_MSG_TYPE_EXPECTED_MISS";\n\n\ +type "ERROR_MSG_EXPECTED_MISS_TYPE_ALIAS" = "ERROR_MSG_TYPE_EXPECTED_MISS";\n\n\ +#[allow(dead_code)]\n\ +const "ERROR_MSG_EXPECTED_MISS_TYPE_CONSTRUCTOR": "ERROR_MSG_EXPECTED_MISS_TYPE_ALIAS" = "ERROR_MSG_TYPE_EXPECTED_MISS";\n\n\ +\n\n");\ + } while(0) + +#define UNEXPECTED_MISS(NAME) \ + do {\ + fprintf(stderr,"Unexpected missing def: " #NAME "\n");\ + printf("pub const " #NAME ": " ERROR_MSG_TYPE_ALIAS " = " ERROR_MSG_TYPE_CONSTRUCTOR ";\n");\ + } while(0) + +#define EXPECT_MISS(NAME) \ + do {\ + fprintf(stderr,"Missing def(but it's expected to be missing): " #NAME "\n");\ + printf("pub const " #NAME ": " ERROR_MSG_EXPECTED_MISS_TYPE_ALIAS " = " ERROR_MSG_EXPECTED_MISS_TYPE_CONSTRUCTOR ";\n");\ + } while(0) + +//#warning "This warning is unseen during `cargo build` unless compilation fails somewhere at build.rs time" + +int main(void) { + printf("/* Commented out ncurses initialization chars: '"); + + //fflush(stdout);fflush(stderr);*((int *)0) = 42; //segfault(on purpose for manual testing purposes from build.rs) here before terminal gets messed up needing a `reset` shell command to restore! and after something's printed to stdout. + + /* some values aren't set until after this is run */ initscr(); endwin(); - printf("\n"); + printf("' */\n"); + + DEFINE_RS_TYPES; /* Success/Failure. */ PCONST(i32, ERR); @@ -19,6 +72,8 @@ int main() { /* Attributes. */ #ifdef NCURSES_ATTR_SHIFT PCONST(u32, NCURSES_ATTR_SHIFT); +#else + UNEXPECTED_MISS(NCURSES_ATTR_SHIFT); #endif /* Colors */ @@ -34,30 +89,44 @@ int main() { /* Values for the _flags member */ #ifdef _SUBWIN PCONST(i32, _SUBWIN); +#else + UNEXPECTED_MISS(_SUBWIN); #endif #ifdef _ENDLINE PCONST(i32, _ENDLINE); +#else + UNEXPECTED_MISS(_ENDLINE); #endif #ifdef _FULLWIN PCONST(i32, _FULLWIN); +#else + UNEXPECTED_MISS(_FULLWIN); #endif #ifdef _SCROLLWIN PCONST(i32, _SCROLLWIN); +#else + UNEXPECTED_MISS(_SCROLLWIN); #endif #ifdef _ISPAD PCONST(i32, _ISPAD); +#else + UNEXPECTED_MISS(_ISPAD); #endif #ifdef _HASMOVED PCONST(i32, _HASMOVED); +#else + UNEXPECTED_MISS(_HASMOVED); #endif #ifdef _WRAPPED PCONST(i32, _WRAPPED); +#else + UNEXPECTED_MISS(_WRAPPED); #endif /* @@ -66,6 +135,8 @@ int main() { */ #ifdef _NOCHANGE PCONST(i32, _NOCHANGE); +#else + UNEXPECTED_MISS(_NOCHANGE); #endif /* @@ -74,6 +145,8 @@ int main() { */ #ifdef _NEWINDEX PCONST(i32, _NEWINDEX); +#else + UNEXPECTED_MISS(_NEWINDEX); #endif /* Keys */ @@ -107,20 +180,30 @@ int main() { PCONST(i32, KEY_ENTER); PCONST(i32, KEY_PRINT); PCONST(i32, KEY_LL); -#ifdef A1 - PCONST(i32, A1); +#ifdef KEY_A1 + PCONST(i32, KEY_A1); +#else + UNEXPECTED_MISS(KEY_A1); #endif -#ifdef A3 - PCONST(i32, A3); +#ifdef KEY_A3 + PCONST(i32, KEY_A3); +#else + UNEXPECTED_MISS(KEY_A3); #endif -#ifdef B2 - PCONST(i32, B2); +#ifdef KEY_B2 + PCONST(i32, KEY_B2); +#else + UNEXPECTED_MISS(KEY_B2); #endif -#ifdef C1 - PCONST(i32, C1); +#ifdef KEY_C1 + PCONST(i32, KEY_C1); +#else + UNEXPECTED_MISS(KEY_C1); #endif -#ifdef C3 - PCONST(i32, C3); +#ifdef KEY_C3 + PCONST(i32, KEY_C3); +#else + UNEXPECTED_MISS(KEY_C3); #endif PCONST(i32, KEY_BTAB); PCONST(i32, KEY_BEG); @@ -180,46 +263,68 @@ int main() { PCONST(i32, KEY_UNDO); PCONST(i32, KEY_MOUSE); PCONST(i32, KEY_RESIZE); + +//TODO: make this 5 line block a 1 line(macro-like) and have rust preprocess it into the 5 lines, so that eg. KEY_EVENT is used only once to avoid duplication and typo or copy/paste mistakes when repeating it. #ifdef KEY_EVENT PCONST(i32, KEY_EVENT); +#else + EXPECT_MISS(KEY_EVENT); #endif PCONST(i32, KEY_MAX); #ifdef NCURSES_MOUSE_VERSION PCONST(i32, NCURSES_MOUSE_VERSION); +#else + UNEXPECTED_MISS(NCURSES_MOUSE_VERSION); #endif #ifdef MASK_SHIFT PCONST(i32, MASK_SHIFT); +#else + EXPECT_MISS(MASK_SHIFT); #endif #ifdef MODIFIER_SHIFT PCONST(i32, MODIFIER_SHIFT); +#else + EXPECT_MISS(MODIFIER_SHIFT); #endif /* Mouse Support */ #ifdef NCURSES_BUTTON_RELEASED PCONST(i32, NCURSES_BUTTON_RELEASED); +#else + UNEXPECTED_MISS(NCURSES_BUTTON_RELEASED); #endif #ifdef NCURSES_BUTTON_PRESSED PCONST(i32, NCURSES_BUTTON_PRESSED); +#else + UNEXPECTED_MISS(NCURSES_BUTTON_PRESSED); #endif #ifdef NCURSES_BUTTON_CLICKED PCONST(i32, NCURSES_BUTTON_CLICKED); +#else + UNEXPECTED_MISS(NCURSES_BUTTON_CLICKED); #endif #ifdef NCURSES_DOUBLE_CLICKED PCONST(i32, NCURSES_DOUBLE_CLICKED); +#else + UNEXPECTED_MISS(NCURSES_DOUBLE_CLICKED); #endif #ifdef NCURSES_TRIPLE_CLICKED PCONST(i32, NCURSES_TRIPLE_CLICKED); +#else + UNEXPECTED_MISS(NCURSES_TRIPLE_CLICKED); #endif #ifdef NCURSES_RESERVED_EVENT PCONST(i32, NCURSES_RESERVED_EVENT); +#else + UNEXPECTED_MISS(NCURSES_RESERVED_EVENT); #endif /* event masks */ @@ -249,22 +354,32 @@ int main() { #ifdef BUTTON5_RELEASED PCONST(i32, BUTTON5_RELEASED); +#else + UNEXPECTED_MISS(BUTTON5_RELEASED); #endif #ifdef BUTTON5_PRESSED PCONST(i32, BUTTON5_PRESSED); +#else + UNEXPECTED_MISS(BUTTON5_PRESSED); #endif #ifdef BUTTON5_CLICKED PCONST(i32, BUTTON5_CLICKED); +#else + UNEXPECTED_MISS(BUTTON5_CLICKED); #endif #ifdef BUTTON5_DOUBLE_CLICKED PCONST(i32, BUTTON5_DOUBLE_CLICKED); +#else + UNEXPECTED_MISS(BUTTON5_DOUBLE_CLICKED); #endif #ifdef BUTTON5_TRIPLE_CLICKED PCONST(i32, BUTTON5_TRIPLE_CLICKED); +#else + UNEXPECTED_MISS(BUTTON5_TRIPLE_CLICKED); #endif PCONST(i32, BUTTON_CTRL); @@ -278,6 +393,11 @@ int main() { PCONSTU(crate::ll::chtype, A_NORMAL); PCONSTU(crate::ll::chtype, A_STANDOUT); PCONSTU(crate::ll::chtype, A_UNDERLINE); +#ifdef A_ITALIC + PCONSTU(crate::ll::chtype, A_ITALIC); +#else + UNEXPECTED_MISS(A_ITALIC); +#endif PCONSTU(crate::ll::chtype, A_REVERSE); PCONSTU(crate::ll::chtype, A_BLINK); PCONSTU(crate::ll::chtype, A_DIM); @@ -285,6 +405,8 @@ int main() { #ifdef A_BLANK PCONSTU(crate::ll::chtype, A_BLANK); +#else + EXPECT_MISS(A_BLANK); #endif PCONSTU(crate::ll::chtype, A_INVIS); @@ -293,4 +415,7 @@ int main() { PCONSTU(crate::ll::chtype, A_ATTRIBUTES); PCONSTU(crate::ll::chtype, A_CHARTEXT); PCONSTU(crate::ll::chtype, A_COLOR); + + //do last, flush just to be sure! + fflush(stdout);fflush(stderr); } diff --git a/src/lib.rs b/src/lib.rs index 06facdee..83a20f69 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,7 +17,7 @@ extern crate libc; use std::mem; use std::{ char, ptr }; use std::ffi::{CString, CStr}; -use self::ll::{FILE_p}; +use self::ll::FILE_p; pub use self::constants::*; pub use self::panel::wrapper::*; pub use self::menu::wrapper::*; @@ -158,7 +158,13 @@ pub fn border(ls: chtype, rs: chtype, ts: chtype, bs: chtype, tl: chtype, tr: ch { unsafe { ll::border(ls, rs, ts, bs, tl, tr, bl, br) } } -#[link_name="box"] pub fn box_(w: WINDOW, v: chtype, h: chtype) -> i32 +// this is 'box' in rust but must use r#box because 'box' is reserved keyword +#[inline(always)] +pub fn r#box(w: WINDOW, v: chtype, h: chtype) -> i32 +{ box_(w,v,h) } + +#[inline(always)] +pub fn box_(w: WINDOW, v: chtype, h: chtype) -> i32 { wborder(w, v, v, h, h, 0, 0, 0, 0) } @@ -1096,7 +1102,6 @@ pub fn prefresh(pad: WINDOW, pmin_row: i32, pmin_col: i32, smin_row: i32, smin_c { unsafe { ll::prefresh(pad, pmin_row, pmin_col, smin_row, smin_col, smax_row, smax_col) } } -#[deprecated(since = "5.98.0", note = "printw format support is disabled. Use addstr instead")] pub fn printw(s: &str) -> Result { // We don't actually need this function to support format strings, @@ -1769,7 +1774,8 @@ pub fn setsyx(y: &mut i32, x: &mut i32) } } -pub fn KEY_F(n: u8) -> i32 +#[inline] +pub const fn KEY_F(n: u8) -> i32 { assert!(n < 16); KEY_F0 + n as i32 diff --git a/src/ll.rs b/src/ll.rs index d6916ecc..24ef141c 100644 --- a/src/ll.rs +++ b/src/ll.rs @@ -62,7 +62,10 @@ extern { pub fn bkgd(_:chtype) -> c_int; pub fn bkgdset(_:chtype); pub fn border(_:chtype,_:chtype,_:chtype,_:chtype,_:chtype,_:chtype,_:chtype,_:chtype) -> c_int; + #[link_name="box"] // points to 'box' of ncurses lib, but is 'box_' here in rust. pub fn box_(_:WINDOW, _:chtype, _:chtype) -> c_int; + //This is 'box' both in rust and in ncurses lib; but 'box' is reserved keyword, so use r#box + pub fn r#box(_:WINDOW, _:chtype, _:chtype) -> c_int; pub fn can_change_color() -> c_bool; pub fn cbreak() -> c_int; pub fn chgat(_:c_int, _:attr_t, _:c_short, _:void_p) -> c_int; @@ -261,8 +264,8 @@ extern { // fn vidputs(_:chtype, extern fn f(c_int) -> c_int) -> c_int; //pub fn vidputs(_:chtype, f:*mut c_char) -> c_int; pub fn vline(_:chtype, _:c_int) -> c_int; - pub fn vwprintw(_:WINDOW, _:char_p, _:va_list) -> c_int; - pub fn vw_printw(_:WINDOW, _:char_p,_:va_list) -> c_int; + pub fn vwprintw(_:WINDOW, fmt:char_p, _:va_list) -> c_int; + pub fn vw_printw(_:WINDOW, fmt:char_p,_:va_list) -> c_int; // fn vwscanw(_:WINDOW, _:char_p, _:va_list) -> c_int; // fn vw_scanw(_:WINDOW, _:char_p, _:va_list) -> c_int; pub fn waddch(_:WINDOW, _:chtype) -> c_int; @@ -423,7 +426,7 @@ extern { } -/// Extended color support. Requires ncurses6. +// XXX: Extended color support. Requires ncurses6. #[cfg(feature = "extended_colors")] extern { pub fn init_extended_color(_: c_int, _: c_int, _: c_int, _: c_int) -> c_int; diff --git a/src/menu/ll.rs b/src/menu/ll.rs index 1541ab80..c722184c 100644 --- a/src/menu/ll.rs +++ b/src/menu/ll.rs @@ -2,7 +2,7 @@ #![allow(unused_imports)] use libc::{c_int, c_char, c_void}; -use ll::{WINDOW, chtype, c_bool}; +use crate::ll::{WINDOW, chtype, c_bool}; pub type MENU = *mut i8; pub type ITEM = *mut i8; diff --git a/src/menu/wrapper.rs b/src/menu/wrapper.rs index b5d72ecc..00a334ef 100644 --- a/src/menu/wrapper.rs +++ b/src/menu/wrapper.rs @@ -1,14 +1,15 @@ #![allow(dead_code)] #![allow(unused_imports)] +#![warn(temporary_cstring_as_ptr)] // false positives? https://github.com/rust-lang/rust/issues/78691 use std::str; use std::ptr; use std::slice; use std::ffi::{CStr, CString}; use libc::*; -use menu::ll; -use ll::{WINDOW, chtype, c_bool}; -use constants::TRUE; +use crate::menu::ll; +use crate::ll::{WINDOW, chtype, c_bool}; +use crate::constants::TRUE; use std::os::raw::c_char; pub type MENU = ll::MENU; @@ -412,7 +413,8 @@ pub fn unpost_menu(menu: MENU) -> i32 { #[cfg(feature="menu")] pub fn menu_request_by_name>>(name: T) -> i32 { unsafe { - super::ll::menu_request_by_name(CString::new(name).unwrap().as_ptr()) + let cs=CString::new(name).unwrap(); + super::ll::menu_request_by_name(cs.as_ptr()) } } diff --git a/src/panel/ll.rs b/src/panel/ll.rs index 30f312ef..348867eb 100644 --- a/src/panel/ll.rs +++ b/src/panel/ll.rs @@ -2,7 +2,7 @@ #![allow(unused_imports)] use libc::{ c_int, c_void }; -use ll::WINDOW; +use crate::ll::WINDOW; pub type PANEL = *mut i8; diff --git a/src/panel/wrapper.rs b/src/panel/wrapper.rs index 8781220d..3de709cb 100644 --- a/src/panel/wrapper.rs +++ b/src/panel/wrapper.rs @@ -1,9 +1,9 @@ #![allow(dead_code)] #![allow(unused_imports)] -use panel::ll; -use ll::WINDOW; -use constants::TRUE; +use crate::panel::ll; +use crate::ll::WINDOW; +use crate::constants::TRUE; pub type PANEL = ll::PANEL;