Skip to content

Feature Detection Is Better than Version Detection

andychu edited this page Dec 29, 2024 · 12 revisions

Summary: Version Detection like checking $BASH_VERSION == "4.4" or User-Agent == "Mozilla" is bad for portability.

Instead, it's better to test if the feature exists:

  • with eval(), in JavaScript, shell, Python, etc.
  • by writing a ./configure script, for C, C++, etc.
    • (preferably without autoconf, although autoconf does follow the philosohpy of feature detection)

  • Comment on Hacker News Thread: Bash Completion in JSON Fields
    • An unnecessary $BASH_VERSION check -- eval 'complete ...' would suffice.
  • All browsers pretend they're Mozilla because web servers detect it
  • LLVM Has to Pretend It's GCC to compile code in the wild
  • Despite its flaws, GNU autoconf (./configure) works because it uses feature detection. If it had used version detection, it would have collapsed from complexity a long time ago, and been a lot less reliable.

NOTE: OSH won't pretend it's bash! It is largely compatible with bash, but not identical.

How Do I Use Feature Detection?

In shell (or JavaScript), an easy and effective way is to use eval. (This is perhaps the best reason to use eval!)

For example, you can test if a shell has declare with something like

if eval 'declare myvar'; then
  have_declare=yes
else
  have_declare=''
fi

# usage:

if test -n "$have_declare"; then
  ...
fi

Observation: eval() and running the C compiler in ./configure are the same thing

In JavaScript, Python, or shell, you might do

eval('await foo()')  # newer JS feature

to probe for a language feature.

In a ./configure script, you might try to compile a file like:

int main() {
  co_yield "foo";   // newer C++ feature
}

2024

This seems like evidence that Rust should support feature detection, not just version detection.

Fish 4.0: The Fish Of Theseus

We have some gripes with [Rust].

Chief among them is how Rust handles portability. While it offers many abstractions over systems, allowing you to target a variety of systems with the same code, when it comes to adapting your code to systems at a lower-level, it’s all based on enumerating systems by hand, using checks like #[cfg(any(target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))].

This is an imperfect solution, allowing you to miss systems and ignoring version differences entirely. From what we can tell, if FreeBSD 12 gains a function that we want to use, libc would add it, but calling it would then fail on FreeBSD 11 without a good way to check, at the moment.

But listing targets in our code is also fundamentally duplicating work that the libc crate (in our case) has already done.

Clone this wiki locally